Here's a component that displays a number (initially 0) and a button that when clicked increments that number. If the number is 10, it also displays a celebratory message.
function App() {
const [count, setCount] = React.useState(0);
const [milestone, setMilestone] = React.useState(false);
const increment = () => {
setCount(count + 1);
if (count === 10) {
setMilestone(true);
} else {
setMilestone(false);
}
};
return (
<div className="App">
<h1>{count}</h1>
<button onClick={increment}>+1</button>
{milestone ? <h1>Happy aniversary!</h1> : null}
</div>
);
}
When I display this, very confusingly, the "Happy anniversary!" message displays when the number on display is eleven, and I can't figure out why.
Because count is ten only on the eleventh click.
you can change your increment function to:
const increment = () => {
const newCount = count + 1
setCount(newCount);
setMilestone(newCount === 10);
};
Or may be use setMilestone within a React.useEffect, like:
React.useEffect(() => {
setMilestone(count === 10);
}, [count])
Related
UPDATE: OMG I'm using setTimeOut. But I still need an answer.
I made an application that accesses an array and outputs each of its elements after a certain period of time. When the array ends, execution stops.
There is a need to pause the execution. How can I do that?
const [isPaused, setIsPaused] = useState(false);
const togglePause = () => {
setIsPaused(!isPaused)
}
const soccData = data.player_positions; // cutting only IDs and positions from data
const [playerPosition, setPlayerPosition] = useState([]); // creating local state to work with IDs and positions
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
useEffect(() => {
getPlayerData(soccData)
return () => {
clearTimeout()
};
}, [soccData]);
return (
<div>{playerPosition}</div>
<p><button onClick={togglePause}></button></p>
)
I tried adding a condition if(!isPaused) to the function getPlayerData (and a dependency to useSeffect), but that didn't work.
Here's my code on codesandbox: https://codesandbox.io/s/youthful-paper-pvrq09
p.s. I found someone's code that allows to start/pause the execution: https://jsfiddle.net/thesyncoder/12q8r3ex/1/, but there's no ability to stop the execution when array ends.
The following works in your sandbox:
export default function App() {
const [isPaused, setIsPaused] = useState(false);
const [frame, setFrame] = useState(0);
const togglePause = () => {
setIsPaused(!isPaused);
};
const playerPosition = data.player_positions[frame];
useEffect(() => {
// do nothing if paused or the last frame is reached
if (isPaused || frame === data.player_positions.length - 1) {
return;
}
const to = setTimeout(() => {
setFrame((f) => f + 1);
}, data.interval);
return () => {
clearTimeout(to);
};
}, [frame, isPaused]);
return (
<div className="App">
<pre>{playerPosition}</pre>
<p>
<button onClick={togglePause}>{isPaused ? "play" : "pause"}</button>
</p>
</div>
);
}
As you can see, I removed the getPlayerData function. Instead, when the frame (which I called the index of the data we're currently at) or the isPaused variable changes, the effect is reran. It first (except for the very first execution) cleans up the still pending timeout (if there is one) and schedules a new one (if not paused and end is not reached) that increments the frame by one.
The main problem with your solution is the following:
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
This does not wait for the first timeout to occur, but instead schedules all frames in advance.
Therefore it is not pausable in a practical way (you could keep track of which timeout already ran, clear all of them when pausing and scheduling only the pending ones again once unpaused but that is not really practical)
setInterval() returns its id on execution. You can pass that id to clearInterval() to cancel it. Something like this might work:
let interval = 0;
interval = setInterval(() => {
// Your program logic
// ...
if(conditionTrueToCancelInterval) {
clearInterval(interval);
}
}, 1000)
import { ChangeEvent, useState } from 'react'
import { useInterval } from 'usehooks-ts'
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}
I have this simple website where I fetch data and get an array of Obj, and display it on the Quiz page, but I also implement a countdown. The problem is that every time the timer renders the fetch run again and I get a different array every sec. How Do I prevent the initial obj I fetch from changing?
Im getting the fetch array from useContext
const Quiz = () =>{
useEffect(() => {
countDown();
}, [num]);
let timer;
const countDown = () => {
if (num > 0) {
timer = setTimeout(() => {
setNum(num - 1);
}, 1000);
}
if(num === 0){
nextQuestion()
}
return num;
};
return(...)
}
You need to remove the num from the dependency array of useEffect.
When you add a dependency to useEffect it will re render every time it changes, and you only want countDown to run once.
Usually you should also wrap the countDown component with useCallback and put the countDown as a dependency on the useEffect
anyway, for now this should solve your issue -
const Quiz = () =>{
useEffect(() => {
countDown();
}, []);
let timer;
const countDown = () => {
if (num > 0) {
timer = setTimeout(() => {
setNum(num - 1);
}, 1000);
}
if(num === 0){
nextQuestion()
}
return num;
};
return(...)
}
does this answer your question ?
import React, { useState, useEffect } from "react";
const questionsList = [
{ question: "Sum of 4+4 ?" },
{ question: "Sum of 10+10 ?" }
];
export default function CountDown() {
const [counter, setCounter] = useState(10);
const [currentQuestion, setCurrentQuestion] = useState(0);
useEffect(() => {
const timeInterval = setInterval(() => {
counter > 0 && setCounter((prevCount) => prevCount - 1);
}, 1000);
if (counter === 0 && currentQuestion + 1 !== questionsList.length) {
setCurrentQuestion((prevQues) => prevQues + 1);
setCounter(10);
}
return () => {
clearInterval(timeInterval);
};
}, [counter]);
return (
<>
<h1>CountDown {counter}</h1>
<h1>
{counter !== 0 ? (
<>
{" "}
Question number {currentQuestion + 1}
{" --> "}
{questionsList?.[currentQuestion]?.question}
</>
) : (
<h1>Test Ended </h1>
)}
</h1>
</>
);
}
I am doing a small project with react lifecycles and I am attempting to change all box colors randomly every time the counter reaches a number divisible by 5 however, the boxes only change when the number is divisible by 10.
App.js:
import React from 'react';
import './App.css';
import Counter from "./components/Counter";
import Box from "./components/Box";
function App() {
const randomColor = [
"#0d0d8c",
"#a41313",
"#064826",
"#996e00"
];
const [number, setNumber] = React.useState(0);
const [boxes, setBoxes] = React.useState([]);
const [color, setColor] = React.useState('red');
const [change, setChange] = React.useState(false);
let box = boxes.map((obj, idx) =>
<Box key={idx} color={color} />
);
let getColor = () => {
if (boxes.length % 5 === 0) {
let rand = Math.floor(Math.random() * randomColor.length);
setColor(randomColor[rand]);
return randomColor[rand];
}
return color;
};
React.useEffect(() => {
if (boxes.length % 5 === 0) {
let rand = Math.floor(Math.random() * randomColor.length);
setColor(randomColor[rand]);
return randomColor[rand];
}
return color;
}, [color])
React.useEffect(() => {
if (number % 2 === 0) {
let newBoxList = [...boxes];
newBoxList.push({color: getColor()});
setBoxes(newBoxList);
}
}, [number,change]);
let reset = () => {
setNumber(0);
setBoxes([]);
setChange(true);
};
return (
<div className="App">
<button onClick={() => setNumber(number + 1)}>Increase</button>
<button onClick={reset}>reset</button>
<Counter count={number}/>
<div className="boxes">{box}</div>
</div>
);
}
export default App;
deployed site: https://priceless-spence-9ae99a.netlify.app/
I think the culprit is your useEffect() hook listening to number. Whenever number is even you are copying the existing contents of boxes and then adding new boxes to the existing boxes; you are not actually updating all boxes in the array. It seems your desire is to have all of those boxes, existing and new, be updated to a new color so you'll need to iterate through the array and update each.
I think it comes down to the fact that you are assigning the return value of getColor to your new boxes rather than the value of local state color.
The Idea is - I press 'start timer' and a function sets the variable 'sessionSeconds' based on whether its work 25mins or rest 5mins session (true/false). Problem is , when interval restarts, function doesn't update this 'currentSessionSeconds'. Maybe it's something with lifecycles or I should use useEffect somehow..
const TimeTracker = () => {
const [work, setWork] = useState(25);
const [rest, setRest] = useState(5);
const [remainingTime,setRemainingTime]= useState(25);
const [iswork, setIswork] = useState(true);
function startTimer() {
clearInterval(interval);
let sessionSeconds = iswork? work * 60 : rest * 60
interval = setInterval(() => {
sessionSeconds--
if (duration <= 0) {
setIswork((iswork) => !iswork)
updateTime(sessionSeconds)
startTimer();
}
}, 1000);
function updateTime(seconds){
setRemainingTime(seconds)
}
}
return (
<div>
<p>
{remainingTime}
</p>
<button onClick={startTimer}>timer</button>
</div>
);
}
I didnt include other code for converting, etc to not over clutter.
I couldn't get your code working to test it since it is missing a }.
I changed your code to include use effect and everything seems to work fine.
https://codesandbox.io/s/wizardly-saha-0dxde?file=/src/App.js
const TimeTracker = () => {
/* In your code you used use state however you didn't change
the state of these variables so I set them to constants.
You can also pass them through props.
*/
const work = 25;
const rest = 5;
const [remainingTime, setRemainingTime] = useState(work * 60);
const [isWork, setIsWork] = useState(true);
const [isTimerActive, setIsTimerActive] = useState(false);
const startTimerHandler = () => {
isWork ? setRemainingTime(work * 60) : setRemainingTime(rest * 60);
setIsTimerActive(true);
};
useEffect(() => {
if (!isTimerActive) return;
if (remainingTime === 0) {
setIsWork((prevState) => !prevState);
setIsTimerActive(false);
}
const timeOut = setTimeout(() => {
setRemainingTime((prevState) => prevState - 1);
}, 1000);
/* The return function will be called before each useEffect
after the first one and will clear previous timeout
*/
return () => {
clearTimeout(timeOut);
};
}, [remainingTime, isTimerActive]);
return (
<div>
<p>{remainingTime}</p>
<button onClick={startTimerHandler}>timer</button>
</div>
);
};
Why is it that the correct count value can be obtained in setinterval after the first click, and then the transformation does not occur again?
import React, { useEffect, useState } from 'react';
const Demo1 = () => {
let [count, setCount] = useState(1);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, []);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
You are directly modifying the state. Instead do this:
setCount(count++)
React doen't really handle setInterval that smoothly, you have to remember that when you put it in componentDidMount (useEffect with an empty dependencies' array), it builds its callback with the current values, then never updates.
Instead, put it inside componentDidUpdate (useEffect with relevant dependencies), so that it could have a chance to update. It boils down to actually clearing the old interval and building a new one.
const Demo1 = () => {
let [count, setCount] = useState(1);
let [intervalId, setIntervalId] = useState(null);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, []);
useEffect(() => {
clearInterval(intervalId);
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, [count]);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
The first thing is that changing the value of state directly like count += 1 is a bad approach, instead use setCount(count + 1) and you cannot console.log any value in the return statement instead use {count} to display the value on the screen instead of console.
The following code will increment the value of count on every click instance
const [count, setCount] = useState(1);
const onCountClick = () => {
// count += 1;
setCount(count + 1);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
});
return (
<div className="App">
<button onClick={() => onCountClick()}>test</button>;
</div>
);