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

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.

Related

React, Uncaught RangeError: Maximum call stack size exceeded

I have an array of objects containing details about players (name, skill(number 0-10)),
I'm trying to split that array into two arrays Team1 and Team2 while making sure that the sum of skill in both teams is equal or close at least.
This is the way I came up with but it seems like I have some kind of recursion error and I can't see where the problem is.
const [team1, setTeam1] = useState([]);
const [team2, setTeam2] = useState([]);
const [team1Skill, setTeam1Skill] = useState(0);
const [team2Skill, setTeam2Skill] = useState(0);
const [skillGap, setSkillGap] = useState(0);
const randomize = () => {
let randomNum1 = Math.floor(Math.random() * playersList.length);
let randomNum2 = Math.floor(Math.random() * playersList.length);
const pickedPlayer1 = playersList[randomNum1];
const pickedPlayer2 = playersList[randomNum2];
if (!team2.includes(pickedPlayer1) && !team1.includes(pickedPlayer1)) {
team1.push(pickedPlayer1);
}
if (!team1.includes(pickedPlayer2) && !team2.includes(pickedPlayer2)) {
team2.push(pickedPlayer2);
}
checkNumberEquality();
if (team1.length + team2.length === playersList.length) {
checkSkillSum();
}
};
const checkNumberEquality = () => {
if (team1.length + team2.length !== playersList.length) {
randomize();
}
};
const checkSkillSum = () => {
team1.map((player) => {
setTeam1Skill((value) => value + player.skill);
});
team2.map((player) => {
setTeam2Skill((value) => value + player.skill);
});
if (team1Skill === team2Skill) {
console.log("The teams are perfectly balanced.");
} else if (
team1Skill + 1 == team2Skill ||
team1Skill + 2 == team2Skill
) {
console.log(
"The teams have been balanced as much as possible, but team 2 is slightly better than team 2"
);
} else if (
team1Skill - 1 == team2Skill ||
team1Skill - 2 == team2Skill
) {
console.log(
"The teams have been balanced as much as possible, but team 1 is slightly better than team 2"
);
} else if (team1Skill !== team2Skill) {
setTeam1([]);
setTeam2([]);
randomize();
}
};
Here's a better and clearer image of the code
Also, in the app I have an input field (in another component) and whenever I type something there I notice that it keeps executing the commands and repeating things ( I kinda know how onChange and re-rendering work in react but it just seemed weird )

Accessing and updating canvas node callback inside of useEffect - React

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.

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

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>
)
}

Logical NaN Error in my React Typescript counter for win percentage

I am making a simple Typescript counter to track my win percentage in my Legends of Runeterra games I play. I can't figure out why when I increment a win or a loss I get NaN as my win percentage. The logic seems fine (obviously you can't decrement right now, that's a problem for later), I just want to focus on fixing the NaN error for now.
Here's my counter component:
import React, { useState } from 'react'
// add a ? after the type name if you want any one of these to be optional, ex: wins?
const Counter: React.FC<{
initialGamesPlayed: number
initialWins: number
initialLosses: number
initialWinPercentage: number
initialDeckName: string
}> = ({
initialDeckName,
initialWinPercentage,
initialWins,
initialLosses,
initialGamesPlayed,
}) => {
const [deckName, setDeckName] = useState(initialDeckName)
const [wins, setWins] = useState(initialWins)
const [losses, setLosses] = useState(initialLosses)
const [totalGames, setTotalGames] = useState(initialGamesPlayed)
const [winPercentage, setWinPercentage] = useState(initialWinPercentage)
const incrementWins = () => {
setWins(wins + 1)
winPercentageCalc()
console.log(winPercentage)
}
const decrementWins = () => {
if (wins > 0) setWins(wins - 1)
winPercentageCalc()
}
const incrementLosses = () => {
setLosses(losses + 1)
winPercentageCalc()
console.log(winPercentage)
}
const decrementLosses = () => {
if (losses > 0) setLosses(losses - 1)
winPercentageCalc()
}
const winPercentageCalc = () => {
setTotalGames(wins + losses)
setWinPercentage((wins / totalGames) * 100)
}
return (
<div>
<p>Deck Name: </p>
<p>wins: {wins} </p>
<button onClick={incrementWins}>+</button>
<button onClick={decrementWins}>-</button>
<p>losses: {losses}</p>
<button onClick={incrementLosses}>+</button>
<button onClick={decrementLosses}>-</button>
<p>Win Percentage: {winPercentage} % </p>
</div>
)
}
export default Counter
Thanks for taking a look!
The setWins, setLosses, setTotalGames and setWinPercentage are all asynchronous functions. So the first time your call winPercentageCalc, this is what happens:
const winPercentageCalc = () => {
setTotalGames(wins + losses) // This is asynchronous, so...
setWinPercentage((wins / totalGames) * 100) // totalGames = 0 => NaN
}
When you divide wins by totalGames, totalGames has not been updated so you divide by 0 which gives NaN (Not a Number) as a result

Categories

Resources