I'm new to learning React and Gatsby, and am trying to find the best way to apply simple a Javascript animation to a DOM element within a component. I know how to handle component events with onClick etc, but say for example I want to continuously change the colour of a <span> in my Header.js component every 2 seconds.
import React from 'react';
export default function Header() {
return (
<header>
<p>This is a <span>test!</span></p>
</header>
)
}
I'd then want to use some JS like:
const spanEl = document.querySelector('header span');
let counter = 0;
const changeColor = () => {
if (counter % 2 == 0) {
spanEl.style.color = "red";
} else {
spanEl.style.color = "blue";
}
counter++;
if (counter == 10) counter = 0;
}
setInterval(changeColor, 2000);
I found that I could put this inside a script tag in html.js before the closing body tag, but is there a way to keep this functionality within the component? Do I need to completely rethink my approach when working within this framework?
If you want to approach this with idiomatic React, then I would recommend expressing this behavior using hooks, component lifecycles, and effects.
The official React docs for hooks and effects are very good, I would start there.
import React from 'react';
const noop = () => null;
// Encapsulate the interval behavior
const useInterval = (callback, delay) => {
const savedCallback = useRef(noop);
useEffect(() => {
savedCallback.current = callback;
savedCallback.current();
}, [callback]);
useEffect(() => {
const id = setInterval(savedCallback.current, delay);
return () => clearInterval(id);
}, [delay]);
};
export default function Header() {
const [color, setColor] = useState("blue");
// setColor causes a re-render of the component
const updateColor = setColor(color === "blue" ? "red" : "blue");
useInterval(updateColor, 2000);
// Use the jsx to change the color instead of reaching into the dom
return (
<header>
<p>This is a <span style={{ color }}>test!</span></p>
</header>
)
}
[EDIT: I've just seen the answer from #windowsill, which I think is better than mine; I would recommend going with that solution.]
In a React functional component, you need to use the useReference hook to target an element (rather than selecting it with document.querySelector()) and the useEffecet hook to set and clear the timeout when the component mounts/unmounts:
import React, {
useEffect,
useRef,
useCallback
} from 'react';
export function Header() {
const animatedText = useRef(null);
const runAnimation = useCallback(elem => {
const currColour = elem.style.color;
elem.style.color = (currColour === 'red' && 'blue') || 'red';
}, []);
useEffect(() => {
const animationInterval = setInterval(() => {
runAnimation(animatedText.current);
}, 2000);
return () => {
clearInterval(animationInterval);
}
}, [runAnimation]);
return (
<header>
<p>This is a <span ref={animatedText}>test!</span></p>
</header>
);
}
The useCallback hook is used for optimization purposes and prevent the function runAnimation from being re-defined and initialized every time the component re-renders.
Related
I'm coming from the world of React and trying to translate a component into React Native. In react we can easily grab an element with useRef and then add or remove classes.
Is there a way to easily translate this to react native as I see when I inspect the current property of the <Text> element it looks nothing like an html element.
Here is the file I'm trying to convert:
import React, {useState, useEffect, useRef} from 'react';
// styles
import styles from './whiteTextReveal.module.css';
export const WhiteTextReveal = props => {
// props
const {text, duration, callback} = props;
// local
const leftMask = useRef(null);
const rightMask = useRef(null);
const textRef = useRef(null);
const animationTimeouts = useRef([]);
useEffect(() => {
reveal();
return () => {
animationTimeouts.current.map(val => clearTimeout(val));
}
}, [text]);
function reveal() {
let time = 0;
// reveal first white masks
const timeout1 = setTimeout(() => {
// cleanup if called successively
textRef.current.classList.remove(styles.shrink);
leftMask.current.classList.remove(styles.moveRight);
rightMask.current.classList.remove(styles.moveLeft);
leftMask.current.classList.add(styles.moveLeft);
rightMask.current.classList.add(styles.moveRight);
}, 1000*time);
animationTimeouts.current = [...animationTimeouts.current, timeout1];
// reveal text behind first white mask
time = time + .8; // come from the css file .mask.left.moveLeft
const timeout2 = setTimeout(() => {
textRef.current.classList.remove(styles.hide);
leftMask.current.classList.remove(styles.moveLeft);
rightMask.current.classList.remove(styles.moveRight);
leftMask.current.classList.add(styles.moveRight);
rightMask.current.classList.add(styles.moveLeft);
}, 1000*time);
animationTimeouts.current = [...animationTimeouts.current, timeout2];
// move mask to cover text again
time = time + .5 + duration; // come from the css file .mask.left.moveRight
const timeout3 = setTimeout(() => {
textRef.current.classList.add(styles.shrink);
const timeout4 = setTimeout(() => {
textRef.current.classList.add(styles.hide);
callback()
}, .7*1000);
animationTimeouts.current = [...animationTimeouts.current, timeout4];
}, time*1000);
animationTimeouts.current = [...animationTimeouts.current, timeout3];
}
return (
<div className={styles.container}>
<span ref={textRef} className={`${styles.text} ${styles.hide}`}>{text}</span>
<div ref={leftMask} className={`${styles.mask} ${styles.left}`}/>
<div ref={rightMask} className={`${styles.mask} ${styles.right}`}/>
</div>
)
};
This is a text animation reveal that paints a white strip over text, slides back, and the scales the text to 0 before telling the parent component it finished.
As you can see from these lines:
textRef.current.classList.remove(styles.shrink);
leftMask.current.classList.remove(styles.moveRight);
rightMask.current.classList.remove(styles.moveLeft);
I am grabbing the ref and then removing classes. And the lines that follow after add those classes back in specific ways.
I am trying to make a typing effect, and its a very simple logic,,,but still i am not able to understand that why the 'e' in hello is always missing everything else is working fine. have not made the blinking cursor yet !!!!
CODE:
import { useState } from "react";
export function Type() {
let str = "hello my name is prateek"
const [char, setChar] = useState("");
function type() {
let i = 0;
let id = setInterval(() => {
setChar(prev => prev + str[i]);
//console.log(i,"i")
// console.log(str[i])
i++;
if (i === str.length - 1) {
//console.log("hello")
clearInterval(id)
}
}, 1000);
}
return (<div>
<h1>{char}</h1>
<button onClick={type}>Type</button>
</div>)
}
OUTPUT
hllo my name is prateek
I think you might have an asynchronous race condition:
Async callbacks go into an event queue.
The delay argument is like a minimum, see MDN for more on reasons delays can take longer
Though I could use help finding the source for dispatcher (source code), I strongly suspect React Hooks like useState use async callbacks and the event queue.
Assuming useState is an async callback, I suspect it has no delay.
One suggestion: use a loop and setTimeout() with 1000*i delay. The loop ensures you'll add each letter, the delay will add each letter ~1s apart.
Another suggestion: Dan Abramov did a really interesting blog post digging into this: Making setInterval declarative with React Hooks (2019). His solution explores a custom hook he writes, useInterval (not part of React Hooks API), and explains what's happening with React render cycles and "sliding delay".
const { useState } = React;
function Type(){
let str = "hello my name is prateek"
const [char,setChar] = useState("");
function type(){
let i = 0;
const id = setInterval(()=>{
setChar(prev=>prev+str[i]);
//console.log(i,"i")
// console.log(str[i])
i++;
if(i === str.length-1){
//console.log("hello")
clearInterval(id)
}
},1000);
}
return <div>
<h2>{char}</h2>
<button onClick={type}>Type</button>
</div>
}
function TypeWorking(){
let str = "hello my name is prateek"
const [char,setChar] = useState("");
function type(){
for(let i=0; i<str.length; i++) {
setTimeout(()=> setChar(prev=>prev+str[i]), 1000*(i+1));
}
}
return <div>
<h2>{char}</h2>
<button onClick={type}>Type</button>
</div>
}
ReactDOM.createRoot(
document.getElementById('app-broken')
).render(<Type />)
ReactDOM.createRoot(
document.getElementById('app-working')
).render(<TypeWorking />)
<h1>Question (bug)</h1>
<div id="app-broken"></div>
<h1>Working</h1>
<div id="app-working"></div>
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
The question also mentions an animated cursor. One idea would be to use ::before/::after pseudo-elements (or another element like a <span>) with an infinite CSS animation.
I reworked Type function and it has worked as what you want. Let's try it.
function type() {
const timerId = setInterval(() => {
setChar((prev) => {
if (prev.length >= str.length) {
clearInterval(timerId);
return prev;
}
return prev + str[prev.length - 1 + 1];
});
}, 200);
}
You can refer at this link: https://codesandbox.io/s/musing-joji-rrpk6i?file=/src/App.js:155-451
I'd like some help understanding why my React Component isn't working. I'm trying work on my React/Js fundamentals by making a basic timer. The component is rendering great, just nothing is happening. I'm looking at the React Profiler on Chrome and my variables seem to be updating correctly.
I know it's basic, I'd just rather start and fix my mistakes and learn on the way.
Any help whatsoever would be greatly appreciated!
import React from 'react'
var sec = 0;
var min = 0;
var hrs = 0;
var timer_id;
// Helper Functions
function tick(){
sec++;
if (sec >= 60){
sec = 0;
min ++;
if (min >= 60){
min = 0;
hrs++;
};
};
}
function add(){
tick();
timer();
}
function timer(){
console.log("TIMER");
timer_id = setTimeout(add, 1000);
}
// Clock Component
class Clock extends React.Component{
render(){
timer();
return (
<div className="clock-face">
<h4> {hrs} : {min} : {sec}</h4>
<button
onClick = {() => clearTimeout(timer_id)}> Stop </button>
</div>
);
}
}
export default Clock;
React components only get rerendered when their props or state changes (or when forced to rerender, but we don't do that). You're updating hrs/min/sec in global variables; those aren't tracked by React.
In addition, it's not a good idea to have render() execute a side effect (timer() in your case); React may elect to render your function more than once.
Here's an example of your clock as a class component that ticks along and cleans the timer up after it's unmounted.
The magic that causes the rerendering here is this.setState being called.
import React from "react";
function divmod(a, b) {
return [Math.floor(a / b), a % b];
}
class Clock extends React.Component {
state = { time: 0 };
componentDidMount() {
this.timer = setInterval(this.tick, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
tick = () => {
this.setState(({ time }) => ({ time: time + 1 }));
};
render() {
const { time } = this.state;
const [hmins, secs] = divmod(time, 60);
const [hrs, mins] = divmod(hmins, 60);
return (
<div className="clock-face">
<h4>
{" "}
{hrs} : {mins} : {secs}
</h4>
</div>
);
}
}
I have a working animation of an object made with the "useRef" hook. Part of the code in this animation will be repeated several times, so I moved it into a separate function, but when I try to call this function, when rendering the component, I get the error "Can't assign to property" scrollLeft "on 1: not an object" what could be the problem?
Full code on codesandbox
https://codesandbox.io/s/peaceful-silence-bm6hx?file=/src/scroll.js
import React, {useState, useEffect, useRef} from 'react'
const Scrollable = props => {
const items = props.items;
let ref = useRef()
const [state, setState] = useState({
isScrolling:false,
clientX:0,
scrollX:0
})
const [touchStart, setTouchStart] = useState(0);
let frameId;
const onMouseDown = e =>{...}
const onMouseUp = e =>{
if(ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault()
let touchShift = touchStart - state.clientX
let rez;
let shift;
if(touchShift > 0) {
shift = 300 - touchShift
rez = state.scrollX + shift
if(rez>2100){
rez =1800
cancelAnimationFrame(frameId)
}
let speed = shift / 20
let cur = state.scrollX
frameId = requestAnimationFrame(animate)
animate(cur,speed,rez)
}
}
const animate = (cur, speed,rez) => {
frameId = requestAnimationFrame(animate)
cur = cur + speed
ref.current.scrollLeft = cur.toFixed(2)
if (Math.round(cur) === rez) {
cancelAnimationFrame(frameId)
setState({
...state,
scrollX:rez,
isScrolling:false,
})
}
}
useEffect(() =>{
document.addEventListener('mousedown',onMouseDown)
document.addEventListener('mouseup',onMouseUp)
return () => {
document.removeEventListener('mousedown',onMouseDown)
document.removeEventListener('mouseup',onMouseUp)
}
})
useEffect(() =>{
ref.current = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(ref.current)
},[]})
return (
<div className={classes.charPage}>
<div
ref={ref}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}>
</div>
</div>
)
}
export default Scrollable;
This error means you're trying to set a property on a number. In your useEffect you're doing this:
ref.current = requestAnimationFrame(animate)
requestAnimationFrame returns, according to MDN:
A long integer value, the request id, that uniquely identifies the entry in the callback list. This is a non-zero value, but you may not make any other assumptions about its value.
But you're also using the same ref for your DOM element. After your useEffect runs it will have set your ref to the rAF id which is a number causing your error when you try to set the scrollLeft property on the ref.
What you can try next to solve this is to use 2 separate refs, one for the requestAnimationFrame and one for your DOM element.
The new React Hooks feature is cool but it sometimes makes me confused. In particular, I have this code wrapped in useEffect hook:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
useEffect(() => {
if (num > 0) check(true);
}, []);
return (//...JSX);
};
The code inside the above useEffect will be executed only once. So what are the differences if I bring the code out of the useEffect, like below:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
if (num > 0) check(true);
return (//...JSX);
};
in the second case the code will be executed at every re-render.
this is a better version of the component:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
useEffect(() => {
if (num > 0) check(true);
}, [num]);
return (//...JSX);
};
In this case the effect (which depends heavily on num) is used only when the num prop has changed.
for reference:
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Anyway, in my opinion using a side effect in this very simple case is overkill!
The code will run faster by checking if num > 0 at every render than checking first if num changed and then if it's > 0..
So you should probably just avoid useEffect and stick to your second piece of code