Using JavaScript to create an animated counter with React.js - javascript

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

Related

React state update before setTimeOut causes timer to end immediately

I am working on a project that visualizes various path algorithms.
The method I chose to update the visual effects were to use document.querySelector(node).classList and update the class to match CSS.
This is the start of the logic, which is triggered by click event.
const visualizeDijkstra = () => {
isVisualizing = true;
const startNode = grid[startNodePos.row][startNodePos.col];
const finishNode = grid[finNodePos.row][finNodePos.col];
const visited = initiateDijkstra(grid, startNode, finishNode);
const path = getPath(finishNode);
animateVisitedAndPath(visited, path);
};
The variables visited and path are arrays containing the result of the algorithm. Each item inside the array is an object containing the coordinates for the grid.
const animateVisitedAndPath = (visited: INode[], path: INode[]) => {
let totalTime = 0;
for (let i = 0; i < visited.length; i++) {
const { row, col } = visited[i];
animateAsVisited(row, col, i, speed, startNodePos, finNodePos);
if (i === visited.length - 1) {
animatePath(path, i, speed, startNodePos, finNodePos);
}
if (i === visited.length - 1) {
totalTime = (i + path.length) * speed;
setTimeout(() => {
isVisualizing = false;
}, totalTime);
}
}
};
Inside animatedVisitedAndPath function, I use a for loop and setTimeOut function to visualize the coordinates which are inside visited and path array.
export function animateAsVisited(
row: number,
col: number,
turn: number,
speed: number,
startNodePos: NodePos,
finishNodePos: NodePos
) {
setTimeout(() => {
const { row: sRow, col: sCol } = startNodePos;
const { row: fRow, col: fCol } = finishNodePos;
if (row === sRow && col === sCol) return;
if (row === fRow && col === fCol) return;
document.getElementById(`node-${row}-${col}`)?.classList.add('visited');
}, speed * turn);
}
And here, I use the document object to access each node, and update the classList.
Everything works fine, but when I want to stop the user to interact with the program using a state variable such as isVisualizing, the moment I update the state before, or after visualizeDijkstra, all of my setTimeOut call back functions invokes immediately, updating all visualizations at the same time when visualizeDijkstra gets invoked.
I apolgize if this is a duplicated question, or the way I ask the question is inappropriate.

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.

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