When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx
Related
I am doing a small React project.
I heard somewhere that using setState(()=>) is better than setState().
For instance, let [count, setCount] = useState(0)
setCount(prev=>prev+1) is better than setCount(count+1)
so I kept using the one having arrow function.
But, here in the code, I found that the arrow function one did not work, but the other one worked.
const addCard = () => {
setCards([
...cards,
{
topic: topicRef.current.value,
desc: descRef.current.value,
id: nanoid(),
done: false,
},
]);
topicRef.current.value = "";
descRef.current.value = "";
};
this code works fine, however if I start doing like setCards(prevState=>[...prevState, {}]), the first render works OK, but after that, the Ref value does not read the value I put properly. (I used useRef() for topic and desc.)
What is the difference between setState(()=>) and setState()?
The more classic example is when you use setInterval inside the useEffect. take a look at the following code:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
let interval = setInterval(() => {
// setCount((c) => c + 1);
setCount(count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
In this case, it renders 0 and then increments the count once after one second. The count value will remain at 1 after that.
The reason behind this is that count here represents your React state. This is a variable that holds a value. A closure is created over the outer scope where count is declared - which is the App component itself. Such variables can change value between function calls, but this is where React's immutability comes into play.
You can update the state using the second method as follows:
useEffect(() => {
let interval = setInterval(() => {
setCount((c) => c + 1); // this will update count value every second
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
There will always be a fresh state for that variable when using this method.
That's why you shouldn't jump into React without learning core JS first. React is not JavaScript, but rather a declarative JavaScript library that simplifies some things.
Codesandbox link of the above example: https://codesandbox.io/s/react-interval-gi4xl7
Great article by Dan referencing the same topic: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
I have this code, i'd like to clearInterval from outside this component (let's assume that I render a button with onClick method that calls clearInterval function).
I tried to pass interval value as a ref, but as soon as state updates this value changes.
useEffect(() => {
(intervalRef.current as ReturnType<typeof setInterval> | undefined) = setInterval(() => {
fetch().then((items) => {
items.forEach((item, id) => {
// set state based on the values
});
});
}, 5000);
return () => clearInterval(intervalRef.current);
}, [state]);
I'm also getting eslint warning for return () => clearInterval(intervalRef.current), so I assume that it won't clear properly inside this method as well.
"The ref value 'intervalRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'intervalRef.current' to a variable inside the effect, and use that variable in the cleanup function."
What is the correct approach of such issues in React?
useEffect(() => {
const intervalId = setInterval(() => {
fetch().then((items) => {
items.forEach((item, id) => {
// set state based on the values
});
});
}, 5000);
return () => clearInterval(intervalId);
}, [state]);
You dont need to make it a ref, if all you want is to clean the interval on unmount. Just assign it to a variable and pass it to your cleanup function.
Note: i see you are planning on doing a state update in promise. you might run into issue where component is unmounted and then the promise callback is triggered which tries to update state. so maybe put a isMounted check
"The ref value 'intervalRef.current' will likely have changed by the
time this effect cleanup function runs. If this ref points to a node
rendered by React, copy 'intervalRef.current' to a variable inside the
effect, and use that variable in the cleanup function."
This is saying to save a reference to the ref value in the useEffect hook's callback closure, to be used in the cleanup function.
useEffect(() => {
(intervalRef.current as ReturnType<typeof setInterval> | undefined) = setInterval(() => {
fetch().then((items) => {
items.forEach((item, id) => {
// set state based on the values
});
});
}, 5000);
const timerId = intervalRef.current;
return () => clearInterval(timerId);
}, [state]);
If you need to, or want to, clear the interval from anywhere else in the component scope, just clear the interval as you would normally with the current ref value:
clearInterval(intervalRef.current);
When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx
We have migrated to 'React Functional Components' instead of 'Class based Component'. I cannot find the substitute logic for setState callback function. I.e, I have a functional component with state, and I want to create an event handler function that mutates the state multiple times in sequence, the caveat being that I dont know the current value of state (it may be true/false). The following example may make more sense.
const Example = () => {
const [ openDoor, setOpenDoor ] = useState(false);
// the following handler should swich 'openDoor' state to inverse of
// current state value. Then after setTimeout duration, inverse it again
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
// within setTimeout below, '!openDoor' does not work because it still
// receives the same value as above because of async nature of
// state updates
setTimeout(() => setOpenDoor(!openDoor), 500)
}
return(...);
}
In class based components, we had callback argument which would update state after previous update. How do I achieve the same in the above functional component using state hook?
I wonder if useEffect is the best solution. Specially when calling setTimeout within useEffect is going to cause an infinite loop since every time we call setOpenDoor, the app renders and then useEffect is called calling again a setTimeOut that will call a setOpenDoor function... Graphically:
setTimeout -> setOpenDoor -> useEffect -> setTimeout -> ... hell
Of course you could use an if statement wihin useEffect the same way that #ksav suggested but that does not accomplish one requirement of #Kayote:
I dont know the current value of state (it may be true/false)
Here is a solution that works without useEffect and accomplish the requirement stated above:
Code working in codesandbox
There, see the importance of this piece of code:
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(openDoor => !openDoor), 500);
};
Since we are using setTimeout, we need to pass callback to setOpenDoor instead of the updated state. This is because we want to send the 'current' state. If we sent the new state instead, by the time that setTimeOut processes that state, it will have changed (because we did it before setTimeOut executes its callback with setOpenDoor(!openDoor);) and no changes will be made.
You can use useEffect hook to see when the state change happend.
useEffect(() => {
// do something
console.log('openDoor change', openDoor)
}, [openDoor]);
I'll tell you that it works pretty much in the same way as this.setState, you just a pass a callback function which takes previous state as a parameter and returns new state(docs)
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
}
return(...);
}
In order for you know when it changes you can use useEffect callback, which's gonna be called each time something changes in the dependencies array(docs)
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
useEffect(() => {
console.log('openDoor changed!', openDoor)
}, [openDoor])
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
}
return(...);
}
:)
You can use useEffect hook to achieve this.
setOpenDoor(!openDoor);
useEffect(() => {
// Here your next setState function
}, [openDoor]);
For more information on hooks please check out https://reactjs.org/docs/hooks-effect.html
You should just using setTimeout within useEffect callback:
const App = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggle = () => setOpenDoor(prevOpen => !prevOpen);
useEffect(() => {
const id = setTimeout(() => toggle(), 1000);
return () => clearTimeout(id);
}, [openDoor]);
return <Container>isOpen: {String(openDoor)}</Container>;
};
import React, { useState, useEffect } from "react";
const Example = () => {
const [openDoor, setOpenDoor] = useState(false);
const toggleOpenDoor = () => {
setOpenDoor(!openDoor);
};
useEffect(() => {
console.log(openDoor);
if (openDoor) {
setTimeout(() => setOpenDoor(!openDoor), 1500);
}
}, [openDoor]);
return (
<>
<button onClick={toggleOpenDoor}>Toggle</button>
<p>{`openDoor: ${openDoor}`}</p>
</>
);
};
export default Example;
Codesandbox
Using hooks, I would like to execute a function only after a particular call to update state. For instance, I would like to achieve the same functionality that this does (assuming I already have already instantiated this piece of state.)
setState({name: Joe}, () => console.log('hi'));
I do not want this to log 'hi' every time that name changes, I only want to log 'hi' after this particular setState call has been executed.
This
const [name, setName] = useState('');
useEffect(() => {
console.log('hi');
}, [name]);
setName('Joe');
setName('Bob'); // only log 'hi' after this one!
setName('Joe');
setName('Bob');
will not work for my purposes because I don't want to log 'hi' every time name changes. The value in the setName call does not matter. The console.log must be executed only after this particular setName call has been executed.
Update: I was overthinking this. I was asking this question because I had a piece of state called "mode" that determined some conditional rendering through a switch statement:
switch(mode) {
case foo: return <Foo />
case bar: return <Bar />
}
I was only wanting to fire some logic when mode was a certain value (aka a certain component would be rendered). I simply moved this logic down a level into the lower-level component and used
React.useEffect(() => {
someLogic();
}, []);
in order to only fire this logic on component render.
State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Just spitballing here
const useStateWithCallback = val => {
const callback = useRef();
const [state,setState] = useState(val);
useEffect(() => {
callback.current && callback.current(state);
callback.current = null;
},[callback,state]);
return [
state,
useCallback((arg,cb) => {
callback.current = cb;
return setState(arg);
},[callback,setState])) // these are safe to omit, right?
]
}
EDIT: not to be too verbose, but usage:
import { useStateWithCallback } from './useStateWithCallback';
const MyCmp = () => {
const [name,setName] = useStateWithCallback('');
...
setName('joe',state => console.log(`Hi ${state}`));
...
}