Accessing and updating canvas node callback inside of useEffect - React - javascript

I've created a canvas which is set to a state using a callback. circles are then created based on mouse x and y that are then drawn to the canvas state after clearing the canvas for each frame. each draw frame reduces the radius of the circles until they fade away
Currently the canvas only updates inside of canvasDraw() on mousemove.
The problem: the canvas needs to be updated using an interval inisde of the useEffect() method so that the circles gradually reduce in radius over time.
for some reason retrieving the canvas state inside of useEffect() returns null. I think this may be becuase the useEffect interval is called once but before the ctx is able to be initialised to a state. But I dont know where to go from here...
The following link was useful in fixing some leaks in my code but still results in a empty state at useEffect():
Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?
import React, { useEffect, useState, useCallback, useReducer } from "react";
const Canvas = () => {
const [isMounted, toggle] = useReducer((p) => !p, true);
const [canvasRef, setCanvasRef] = useState();
const [ctx, setCtx] = useState();
const handleCanvas = useCallback((node) => {
setCanvasRef(node);
setCtx(node?.getContext("2d"));
}, []);
const [xy, setxy] = useState(0);
const [canDraw, setCanDraw] = useState(false);
const [bubbles, setBubbles] = useState([]);
const canvasDraw = (e) => {
setxy(e.nativeEvent.offsetX * e.nativeEvent.offsetY);
xy % 10 == 0 ? setCanDraw(true): setCanDraw(false);
canDraw && createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
setBubbles(bubbles => [...bubbles, bubble])
}
const drawBubbles = useCallback(() => {
if (ctx != null){
ctx.clearRect(0,0,canvasRef.width,canvasRef.height);
bubbles.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
}, [])
}
});
useEffect(() => {
const interval = setInterval(() => {
console.log(ctx);
drawBubbles(ctx); // <------ THE PROBLEM (ctx is null)
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
{isMounted && <canvas
onMouseMove={canvasDraw}
ref={handleCanvas}
width={`1280px`}
height={`720px`}
/>}
</section>
</main>
);
}
export default Canvas

Something I learned the hard way with useEffect and useState is that you have to be very careful about what variables are actually being closed over every time the component rerenders.
It's simpler to understand with an example:
export const SimpleExample = () => {
const [ fruit, setFruit ] = useState('apple')
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [])
return (<div>
<p>{fruit}</p>
<button onClick={() => setFruit('orange')}>Make Orange</button>
<p>The console will continue to print "apple".</p>
</div>)
}
Here, we are printing the value of fruit every second. At first it is "apple", and there's a button to change it to "orange". But what really happens when the button is clicked?
Before clicking the button, SimpleExample is run as a function, creating a variable called fruit and initializing it to "apple".
The useEffect callback is invoked, setting up an interval that closes over the fruit variable. Every second, "apple" is printed to console.
Now we click the button. This calls setFruit which will force the SimpleExample function to rerun.
A new fruit variable is created, this time with a value of "orange". The useEffect is not invoked this time, as no dependencies have changed.
Because the useEffect closed over the first fruit variable created, that's what it continues to print. It prints "apple", not "orange".
The same thing is happening in your code, but with the ctx variable. The effect has closed over the ctx variable when it was null, so that's all it remembers.
So how do we fix this?
What you should do depends on the use case. For example, you could reinvoke the effect by supplying it a dependency on ctx. This turns out to be a good way to solve our fruit example.
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [fruit])
In your particular case, however, we're dealing with a canvas. I'm not 100% sure on what the best practices are with canvas and React, but my intuition says that you don't necessarily want to rerender the canvas every time a bubble is added (recall: rerender happens anytime set* functions are called). Rather, you want to redraw the canvas. Refs happen to be useful for saving variables without triggering rerenders, giving you full control over what happens.
import React, { useEffect, useRef } from "react";
const Canvas = () => {
const canvasRef = useRef()
const bubbles = useRef([])
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
bubbles.current = [...bubbles.current, bubble]
}
const drawBubbles = () => {
const ctx = canvasRef.current?.getContext('2d')
if (ctx != null) {
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
bubbles.current.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
})
}
};
const canvasDraw = (e) => {
const canDraw = (e.nativeEvent.offsetX * e.nativeEvent.offsetY) % 10 == 0
if (canDraw) createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
useEffect(() => {
const interval = setInterval(() => {
drawBubbles(canvasRef.current?.getContext('2d'));
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
<canvas
onMouseMove={canvasDraw}
ref={canvasRef}
width={`1280px`}
height={`720px`}
/>
</section>
</main>
);
}
export default Canvas
In this case, there is only ever one single canvasRef and bubbles variable since they are defined as references. Therefore, the effect only closes over the one instance and always has access to the current value of the reference, even if it changes.

Related

Can do a transform whenever the useState is updated? Only vanilla react

I working on this streamer leaderboard project that only allows vanilla React. However, you need to move the streamer up or down in real-time whenever their points go up or down in rank. Since this is frontend test, I only used a Math.random() to change the streamer's points state. The animation would be like this link below(of course without clicking). I was thinking CSS animations, but not really sure how I can connect an animation to a useState update. Below, is a bit of my code as a reference.
Animation Example
const [streamerList, setStreamersList] = useState<StreamerType[]>([]);
useEffect(() => {
const getData = async () => {
const res = await fetch(
'https://webcdn.17app.co/campaign/pretest/data.json'
);
const data = await res.json();
const item = data;
setStreamersList(item);
//error catch handle
};
getData();
}, []);
//for now lets just add the fetching and later move it into a new folder
useEffect(() => {
const randomizer = () => {
let newArr = [...streamerList];
const eleSelect = Math.floor(Math.random() * (streamerList.length - 1));
const status = Math.floor(Math.random() * 2);
switch (status) {
case 0:
newArr[eleSelect].score =
newArr[eleSelect].score + Math.floor(Math.random() * 500);
newArr = newArr.sort((a, b) => b.score - a.score);
setStreamersList(newArr);
break;
case 1:
newArr[eleSelect].score =
newArr[eleSelect].score - Math.floor(Math.random() * 500);
newArr = newArr.sort((a, b) => b.score - a.score);
setStreamersList(newArr);
break;
default:
console.log('test');
}
};
//need a randomizer which element in the array
const interval = setInterval(() => {
randomizer();
//add all the other randomize && we do the set add /sub the points here
//TODO: there is a gap between adding, it seems like it should be none should nonexistent
}, 100);
return () => clearInterval(interval);
}, [streamerList.length, streamerList]);
return (
// TODO: unique key prop is have issues here for some reason
<LeaderBoard>
{streamerList.map((ele, index) => (
<Streamer
number={index}
userId={ele.userID}
displayName={ele.displayName}
picture={ele.picture}
score={ele.score}
/>
))}
</LeaderBoard>
);

Multiple States in React doesn't update which are dependent on other state

I am trying to update the State B when the value of State A fulfills the criteria but the state B isnt changing.
If the below code is run the value of B should increase by +1 whenever A reaches 100 , But when displayed the value of B remains fixed to 0
Here the pseudo code of what i am trying to do
import React , {useState} from "react";
export default function Test() {
const [A , setA] = useState(0);
const [B, setB] = useState(0);
const handleStart = () => {
setInterval( () => {
setA(prev => prev + 1)
if( A === 100){
setB(prev => prev +1)
A = 0
}
},10)
}
return (
<div>
<h1>{A}</h1>
<h1>{B}</h1>
<button onClick = {() => {handleStart()}}> START </button>
</div>
);
}
setA() method is an async method which is updating values asynchronously so your if condition before the setA may trigger first as React creates a batch of these async task. You need to move the logic into SetA() to make it working.
Code -
const handleStart = () => {
setInterval(() => {
setA((prev) => {
if (prev === 100) {
setB((prevB) => prevB + 1);
return 0;
}
return prev + 1;
});
}, 10);
};
Working Example - codesandbox Link

React JS: State become 'undefined' when working with useEffect

I am working on typewriter effect in React JS. I have previously implemented typewriter effect in Vanilla JS.
The React JS code that I am writing is
import React, { useEffect, useRef, useState } from 'react'
const Typewriter = () => {
const el = useRef();
const words = ["Life", "Could Be", "A Dream"];
const [index, setIndex] = useState(0);
const [subIndex, setSubIndex] = useState(0);
const [isEnd, setIsEnd] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [duration, setDuration] = useState(200);
const [currentWord, setCurrentWord] = useState("");
const spedUp = 50;
const normalSpeed = 200;
useEffect(() => {
const interval = setInterval(() => {
setIndex(index % words.length);
setIsEnd(false);
if (!isDeleting && subIndex <= words[index].length) {
setCurrentWord((prev) => prev.concat(words[index][subIndex]));
setSubIndex((prev) => prev += 1);
el.current.innerHTML = currentWord;
}
if (isDeleting && subIndex > 0) {
setCurrentWord((prev) => prev.substring(0, prev.length - 1));
setSubIndex((prev) => prev -= 1);
el.current.innerHTML = currentWord;
}
if (subIndex === words[index].length) {
setIsEnd(true);
setIsDeleting(true);
}
if (subIndex === 0 && isDeleting) {
setCurrentWord("");
setIsDeleting(false);
setIndex((prev) => prev += 1);
}
setDuration(isEnd ? 1500 : isDeleting ? spedUp : normalSpeed);
}, duration);
return () => clearInterval(interval);
}, [subIndex, index, currentWord, isEnd, isDeleting, duration, words])
return (
<>
<h1 ref={el}>Placeholder text</h1>
</>
)
}
export default Typewriter
When I run the React App, the phrases/words are appended with 'undefined' at the end. The application displays the 3 phrases once and then the app crashes because the code tries to get the length of an undefined array element.
Here are the visuals
React Application GIF
Can someone explain to me what is happening and are the dependencies used for useEffect hook correct or not?
Main issue
You are using set(X) and making an assumption that the value will be immediately set and that you can base later conditions on it. useState does not actually set the state immediately -- it happens on the next loop, so any conditions you have (like testing the index after setIndex) are going to go awry because they're using stale values. See useState set method not reflecting change immediately
I avoided this by using interim values (newIndex, newSubIndex) -- I'm not wild about this approach -- I'd probably refactor with a different strategy, but it was the quickest way to get this up and running.
import React, { useEffect, useRef, useState } from 'react'
const Typewriter = () => {
const words = ["Life", "Could Be", "A Dream"];
const [index, setIndex] = useState(0);
const [subIndex, setSubIndex] = useState(0);
const [isEnd, setIsEnd] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [duration, setDuration] = useState(200);
const [currentWord, setCurrentWord] = useState("");
const spedUp = 50;
const normalSpeed = 200;
useEffect(() => {
const interval = setInterval(() => {
let newSubIndex = subIndex;
let newIndex = index % words.length;
setIsEnd(false);
if (!isDeleting && subIndex < words[newIndex].length) {
setCurrentWord((prev) => prev.concat(words[newIndex][subIndex]));
newSubIndex += 1;
}
if (isDeleting && subIndex > 0) {
setCurrentWord((prev) => prev.substring(0, prev.length - 1));
newSubIndex -= 1;
}
if (newSubIndex === words[newIndex].length) {
setIsEnd(true);
setIsDeleting(true);
}
if (newSubIndex === 0 && isDeleting) {
setCurrentWord("");
setIsDeleting(false);
newIndex += 1;
}
setSubIndex(newSubIndex);
setIndex(newIndex);
setDuration(isEnd ? 1500 : isDeleting ? spedUp : normalSpeed);
}, duration);
return () => clearInterval(interval);
}, [subIndex, index, currentWord, isEnd, isDeleting, duration, words])
return (
<>
<h1>{currentWord}</h1>
</>
)
}
export default Typewriter
Minor issue
In React, rather than using a ref and editing innerHTML, normally the content is just inlined in the rendered DOM structure. See <h1>{currentWord}</h1>
You may want to look into storing your state in a larger object rather than everything being a separate useState. You may also want to look into useReducer.

Using JavaScript to create an animated counter with React.js

I have four counters that I would like to animate (incrementing the count from 0 to a specific number) using JavaScript. My code is the following:
const allCounters = document.querySelectorAll('.counterClass');
counters.forEach(allCounters => {
const updateCounter = () => {
const end = +allCounters.getAttribute('data-target');
const count = +allCounters.innerText;
const increment = end / 200;
if (count < end) {
allCounters.innerText = count + increment;
setTimeout(updateCounter, 1);
} else {
allCounters.innerText = end;
}
};
updateCounter();
});
In React, I wasn't sure how to get it to run. I tried including the code after the using dangerouslySetInnerHTML, but that's not working. (I'm new to React).
I appreciate any assistance you could give me. Thanks so much!
Right before I posted my question, I found a plug-in (https://github.com/glennreyes/react-countup) that could do it, but wondered if it's still possible using JS. Thanks!
While using React, try to avoid direct DOM operations (both query and modifications). Instead, let React do the DOM job:
const Counter = ({start, end}) = {
// useState maintains the value of a state across
// renders and correctly handles its changes
const {count, setCount} = React.useState(start);
// useMemo only executes the callback when some dependency changes
const increment = React.useMemo(() => end/200, [end]);
// The logic of your counter
// Return a callback to "unsubscribe" the timer (clear it)
const doIncrement = () => {
if(count < end) {
const timer = setTimeout(
() => setCount(
count < (end - increment)
? count + increment
: end
),
1);
return () => clearTimeout(timer);
}
}
// useEffect only executes the callback once and when some dependency changes
React.useEffect(doIncrement, [count, end, increment]);
// Render the counter's DOM
return (
<div>{count}</div>
)
}
const App = (props) => {
// Generate example values:
// - Generate 5 counters
// - All of them start at 0
// - Each one ends at it's index * 5 + 10
// - useMemo only executes the callback once and
// when numCounters changes (here, never)
const numCounters = 5;
const countersExample = React.useMemo(
() => new Array(numCounters)
.fill(0)
.map( (c, index) => ({
start: 0,
end: index*5 + 10,
})
),
[numCounters]
);
return (
<div id="counters-container">
{
// Map generated values to React elements
countersExample
.map( (counter, index) => <Counter key={index} {...counter}/> )
}
</div>
)
}

setInterval updating state in React but not recognizing when time is 0

I am practicing React useState hooks to make a quiz timer that resets every ten seconds. What I have now is updating the state each second, and the p tag renders accordingly. But when I console.log(seconds) it shows 10 every time, and so the condition (seconds === 0) is never met . In Chrome's React DevTools, the state is updating accordingly as well. What am I doing wrong here?
import React, {useState } from 'react';
function App() {
const [seconds, setSeconds] = useState(10);
const startTimer = () => {
const interval = setInterval(() => {
setSeconds(seconds => seconds - 1);
// Logs 10 every time
console.log(seconds)
// Never meets this condition
if (seconds === 0) {
clearInterval(interval)
}
}, 1000);
}
return (
<div>
<button onClick={() => startTimer()}></button>
// updates with current seconds
<p>{seconds}</p>
</div>
)
}
export default App;
That is because the setSeconds updates the state with a new variable on every tick but the initial reference (seconds === 10) still points to the initial set variable. That is why it stays at 10 => The initial reference.
To get the current value, you have to check it in the setSeconds variable (passed as seconds) like this:
const startTimer = () => {
const interval = setInterval(() => {
setSeconds(seconds => {
if(seconds < 1) {
clearInterval(interval);
return 10;
}
return seconds - 1;
});
}, 1000);
}
Here is a working sandbox
You should also rename the variable to not have the same name (seconds) twice in a single funtion.

Categories

Resources