Referencing outdated state in React useEffect hook
I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.
The reason for this is due to closures. A closure is a function's reference to the variables in its scope. Your useEffect
callback is only ran once when the component mounts and hence the return callback is referencing the initial count value of 0.
The answers given here are what I would recommend. I would recommend @Jed Richard's answer of passing [count]
to useEffect
, which has the effect of writing to localStorage
only when count changes. This is better than the approach of not passing anything at all writing on every update. Unless you are changing count extremely frequently (every few ms), you wouldn't see a performance issue and it's fine to write to localStorage
whenever count
changes.
useEffect(() => { ... }, [count]);
If you insist on only writing to localStorage
on unmount, there's an ugly hack/solution you can use - refs. Basically you would create a variable that is present throughout the whole lifecycle of the component which you can reference from anywhere within it. However, you would have to manually sync your state with that value and it's extremely troublesome. Refs don't give you the closure issue mentioned above because refs is an object with a current
field and multiple calls to useRef
will return you the same object. As long as you mutate the .current
value, your useEffect
can always (only) read the most updated value.
CodeSandbox link
const {useState, useEffect, useRef} = React;
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Count in console is not always 0</div>}
</div>
);
}
function Content(props) {
const value = useRef(0);
const [count, setCount] = useState(value.current);
useEffect(() => {
return () => {
console.log('count:', value.current);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button
onClick={() => {
value.current -= 1;
setCount(value.current);
}}
>
-1
</button>
<button
onClick={() => {
value.current += 1;
setCount(value.current);
}}
>
+1
</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
Your useEffect callback function is showing the initial count, that is because your useEffect is run only once on the initial render and the callback is stored with the value of count that was present during the iniital render which is zero.
What you would instead do in your case is
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
});
In the react docs, you would find a reason on why it is defined like this
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
Read the react docs on Why Effects Run on Each Update
It does run on each render, to optimise it you can make it to run on count
change. But this is the current proposed behavior of useEffect
as also mentioned in the documentation and might change in the actual implementation.
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, [count]);
This will work - using React's useRef - but its not pretty:
function Content(props) {
const [count, setCount] = useState(0);
const countRef = useRef();
// set/update countRef just like a regular variable
countRef.current = count;
// this effect fires as per a true componentWillUnmount
useEffect(() => () => {
console.log("count:", countRef.current);
}, []);
}
Note the slightly more bearable (in my opinion!) 'function that returns a function' code construct for useEffect.
The issue is that useEffect copies the props and state at composition time and so never re-evaluates them - which doesn't help this use case but then its not what useEffects are really for.
Thanks to @Xitang for the direct assignment to .current for the ref, no need for a useEffect here. sweet!