What difference between direct argument and callback in setState?
I've heard that the react scheduler only correctly exposes component updates when a callback is used. So that they hit the tick 16ms
const [state, setState] = useState(null)
function handle() {
setState(true)
// or
setState(() => true)
}
Using the callback form allows you to use the previous value in state, even if the previous value hasn't been rendered yet. For example:
const [counter, setCounter] = useState(0);
const someHandler = () => {
setCounter(counter + 1);
setCounter(counter + 1);
}
will only end up increasing counter by one, because the counter that's in scope when the handler runs is from a single render. It's like doing setCounter(1); setCounter(1), so counter ends up being 1, not 2.
Using the callback form allows you to use the previous state:
setCounter(counter => counter + 1);
setCounter(counter => counter + 1);
will properly increase counter by 2, not just 1.
Another difference is that using the callback form allows you to put functions into state:
const [fn, setFn] = useState();
const fnToPutInState = () => {
console.log('fn');
};
// ...
setFn(fnToPutInState);
will result in the function being invoked immediately, rather than setting state. The solution is to return the function from a callback instead:
setFn(() => fnToPutInState);
Related
Hello i am struggling to set dynamic time for settimeout function in react js.
i have long string of key value pair of time and message. i wants to display each message for specific time and loop through whole list.
here is what i am trying, but not working.
const [timer, setTimer] = useState(0)
const [time, setTime] = useState(5000)// this is default value to start which need to update with str time value
const str=[{name:"rammy", time:1000},
{name:"james", time:4000},
{name:"crown", time:2000}]
useEffect(()=>{
const getTime= str[timer].time
setTime(getTime)
},[timer])
//when timer change it should update update time state which will be used to update time for time settime out
function increment() {
useEffect(()=>{
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
},[timer])
} // above code is for increment time state on each iteration
function ButtonHandle(){
//setRealString(itr)
increment()
} //button handler for start timer
First of all, you can't put hooks inside functions (other than your component functions). https://reactjs.org/docs/hooks-rules.html
So take the useEffect out of increment()
useEffect(()=>{
increment()
},[timer])
function increment() {
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
}
But you also need to clear the timeout. We can return the timeout function to reference it, and clear the time out with a return inside useEffect. Clearing timeouts and intervals in react
useEffect(()=>{
const myTimeout = increment()
return () => {
clearTimeout(myTimeout)
}
},[timer])
function increment() {
return setTimeout(() => {
setTimer((ele) => ele + 1);
}, time);
}
Then we can combine the useEffects which both have a dependancy array of [timer].
useEffect(() => {
const getTime = str[timer].time;
setTime(getTime);
const myTimeout = increment();
return () => {
clearTimeout(myTimeout);
};
}, [timer]);
You don't need to use useEffect to do it. You misunderstood the useEffect usage, it's a react hook to you implement side-effects and you can't use react-hooks inside a function, it should be in the component scope.
I can increment directly from the ButtonHandle function.
// On the index state is necessary in this implementation
const [index, setIndex] = useState(-1)
const guys=[
{name: "rammy", time:1000},
{name: "james", time:4000},
{name: "crown", time:2000}
]
// useCallback memoize the increment function so that it won't
// change the instance and you can use it in the useEffect
// dependency array
const increment = useCallback(() => {
setIndex((i) => i+1)
}, [])
useEffect(() => {
// change the state if the timer is greater than -1
if (index !== -1) {
if (index >= guys.length) {
setIndex(-1);
} else {
setTimeout(() => {
increment();
}, guys[index].time); // <-- you get the time from your array
}
}
}, [index, increment]);
function handleClick(){
//setRealString(itr)
increment()
}
Even though I helped you, I don't know what you're trying to do. This implementation sounds like a code smell. We can help you better if you explain the solution you're trying to do instead of just the peace of code.
You don't need to set the time state, as you already have the time in the array; avoiding unnecessary state changes is good.
I have this code here:
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => {
console.log({ step }, "reff.current before incr ", reff.current);
reff.current = reff.current + step;
console.log("ref.curret after incr ", reff.current);
return c + step;
});
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => setStep((prevStep) =>(prevStep + 1))}
value="+ step"
/>
</>
);
}
You can view it here: https://codesandbox.io/s/gracious-roentgen-31ntb?file=/src/App.js:0-817
Note that I'm incrementing ref inside setCount callback
When the component loads a setInterval is started. If I trigger the useEffect again by incrementing step before 6 seconds pass, it clears the setInterval and creates a new one.
And now if I don't increment step again and wait for 6 seconds, I see step: 1 reff.current before incr, 0 for the first time the setInterval callback is called.
After incrementing in the next line(reff.current = reff.current + step) I see "ref.curret after incr ", 1
When the setInterval callback is called again after 6 seconds, I see
step: 1 reff.current before incr, 2 //how did this become 2
I don't understand how the value of reff.current is 2.
This only happens when I increment the step(which clears the first interval). If I set the initial step to 1 and don't increment it, I see expected values.. Just checked again. It doesn't work as expected.
I can't understand why the value of reff.current is 2 when the setInterval callback is called the second time.
Go to the sandbox link and
Click on the +step button once
Open the console and see
log 1:{step: 1} "reff.current before incr "0
log 2: ref.curret after incr 1
//second time callback is called
log 3: {step: 1} "reff.current before incr "2 //this should be 1
log 4: ref.curret after incr 3```
Your problem happens because you are performing a side effect (updating reff.current) inside the updater function passed to the setCount.
The beta react docs say that:
Updater functions run during rendering, so updater functions must be
pure and only return the result. Don’t try to set state from inside of
them or run other side effects. In Strict Mode, React will run each
updater function twice (but discard the second result) to help you
find mistakes.
You are running your app in StrictMode. To verify it comment it:
const rootElement = document.getElementById('root');
ReactDOM.render(
// <StrictMode>
<App />,
// </StrictMode>,
rootElement
);
and the problem doesn't happen anymore.
Can you please try out this code below,
Not sure about the exact reason why the value was incremented, but using it directly instead of returning from a function, did work as expected. Also it works for further step increment.
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
console.log({ step }, "reff.current before incr ", reff.current);
debugger;
// ------ modified here
let newVal = reff.current + step;
reff.current = newVal;
console.log("ref.curret after incr ", reff.current);
setCount(newVal);
// ------ modified here
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => {
e.preventDefault();
setStep((prevStep) => {
console.log(prevStep);
return Number(prevStep) + 1;
});
}}
value="+ step"
/>
</>
);
}
The image of the log
const [ change , setChange ] = useState(0);
const funct = () => {
if (change === 100){
return 0;
}
setChange((pre) => {
return pre + 10
});
funct()
}
if I call the function fuct() when any event occur , it will have to call itself (recursive function) until the value of change become 100. But here the function is running infinitely(infinte recursion). This is because the state variable is not changing at every instant of setChange() call.
WHY ?
WHY DID THE STATE IS NOT CHANGED BETWEEN RECURSIVE FUNCTION CALLS ?
WHY ? WHY DID THE STATE IS NOT CHANGED BETWEEN RECURSIVE FUNCTION CALLS ?
Since funct() is triggered via React-based event, state updates are batched.
In order not to batch state update, the trigger should come outside of React-based events, like setInterval().
With that said, here's an example using useEffect() hook with setInterval().
const {useState, useEffect} = React;
const App = (props) => {
const [change, setChange] = useState(0);
useEffect(() => {
const t = setInterval(() => (
setChange((change) => (change + 10))
), 1000);
return () => clearInterval(t);
}, [change]);
return (
<div>{`Change: ${change}`}</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is because the dispatch function of useState (setChange in this case) is a async operation and doesn't update the state as soon as it is called. You can try this code instead:
const [change, setChange] = useState(0);
const funct = (pre = 0) => {
if (pre === 100) {
return 0;
}
setChange(pre + 10);
funct(pre + 10);
}
Here, the use of setChange can be avoided but, considering your original code might required that, I have provided the solution.
Please make a note! The state will never be useful until the line of codes is left to process. you cannot use useState in that way:
const [mystate, setState] = useState(0);
const getSum = () => {
setState(10);
sum = mystate + 10 //And expect 20? No, you will never get 20 but you will receive 10.
}
always complete your code using let or var and send back the results to the state.
const [ change , setChange ] = useState(0);
let iter = 0;
const funct = () => {
if (iter === 100){
setChange(iter);
}
else{
iter=iter+10
funct();
}
}
You cannot use "change" to return the count value every time the function runs but you can use "iter"! In the end you will have change with total values.
setState/usestate are Asynchronous functions i.e. we can't setState on one line and assume state has changed on the next.
You can try this:
useEffect(() => {
if(change!==0){
funct();
}
}, [change])
const funct = () => {
if (change === 100){
return 0;
}
setChange((pre) => {
return pre + 10
});
}
There is a component "DateForm" that changes the global state of "counterInfo" when the form is submitted.
//DateForm component submittal function.
const submitDate = () =>{
props.setCounterInfo(dateInfo); //passes date info to be counterInfo state in App.js
props.setShowInputForm(false); //DateInfo component is no longer rendered
}
then, in app.js the counterInfo state is passed to the Timer component
const App = () => {
const [showInputForm, setShowInputForm] = useState(false);
const [counterInfo, setCounterInfo] = useState(undefined);
return (
<>
<Timer
counterInfo = {counterInfo}
></Timer>
{showInputForm &&
<DateForm
setShowInputForm = {setShowInputForm}
setCounterInfo = {setCounterInfo}
></DateForm>}
</>
);
}
There is a useEffect hook inside of the Timer function that, on a one second interval, used the value of counterInfo.
//Inside the Timer Component
const [currTime, setCurrTime] = useState(null);
useEffect (() => {
setInterval(() => {
let timeLeft = (new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime();
setCurrTime(timeLeft);
},1000);
return(clearInterval());
}, [props, setCurrTime]);
What I intended to happen is for the value of timeLeft in Timer.js to update when the value of counterInfo is updated in DateForm, however, when the value is changed in DateForm, the result of both the new value of counterInfo and the old one both flash when the value of timeLeft is used in Timer.js. This issue isn't caused by any code in Timer.js becuase I tried moving the useEffect hook to app.js and passing the value down to Timer but the problem persisted. The only place that the setCounterInfo state is changed is in the DateForm component.
Does anyone have any idea how to fix this?
First, you have bit mis-syntax at interval decleration
useEffect (() => {
let interval = setInterval(() => {...},1000);
return () => clearInterval(interval);
}, [props, setCurrTime]);
But unrelated, React by default re-applies effects after every render. This is intentional and helps avoid a whole class of bugs that are present in React components.
When it comes to intervals, its specifical matters cause if a render was applied every time setInterval is called, it never will get a chance to actually run
In other words, this code might have some side effects as useEffect in each run cares only of the existing values in that time and forget everything else, and interval isn't like so.
For that from my point of view the best practice is to create useInterval custom hook, that inside will store the callback for the meanwhile
function useInterval(callback) {
const savedCallback = React.useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function run() {
savedCallback.current();
}
let interval = setInterval(run ,1000);
return () => clearInterval(interval);
}, [])
}
//Inside the Timer Component
const [currTime, setCurrTime] = useState(null);
useInterval(()=>
setCurrTime((new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime()))
I would like to know the difference between the following two versions of code. Both versions do the same.
1) Here just the counter variable is used to get the current value
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(counter + 1)}>{counter}</button>;
}
2) This version passes a function to setCounter
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(c => c + 1)}>{counter}</button>;
}
The official documentation says:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So what's wrong with the first option? Are there some pitfalls?
With the particular code in your example, you have the previous value in hand, so there isn't much difference. But sometimes you don't. For example, suppose you wanted to have a memoized callback function. Due to the memoization, the value of counter gets locked in when the closure is created, and won't be up to date.
const Counter = () => {
const [counter, setCounter] = useState(0);
// The following function gets created just once, the first time Counter renders.
const onClick = useCallback(() => {
setCounter(c => c + 1); // This works as intended
//setCounter(counter + 1); // This would always set it to 1, never to 2 or more.
}, []); // You could of course add counter to this array, but then you don't get as much benefit from the memoization.
return <button onClick={onClick}>{counter}</button>;
}