I want to save state to localStorage
when a component is unmounted.
This used to work in componentWillUnmount
.
I tried to do the same with the useEffect
hook, but it seems state is not correct in the return function of useEffect
.
Why is that? How can I save state without using a class?
Here is a dummy example. When you press close, the result is always 0.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Why is count in console always 0 ?</div>}
</div>
);
}
function Content(props) {
const [count, setCount] = useState(0);
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector("#app"));
CodeSandbox
While there's currently no React Hook that does this out of the box, you can manually retrieve either the previous state or props from within a functional component by leveraging the useRef , useState , usePrevious , and useEffect Hooks in React.
@alaboudi Yes.. as Shubham Khatri said it will render again. but you can skip calling your effect after the re-rendering using the second argument refer reactjs.org/docs/…
Passing no 2nd argument causes the useEffect to run every render. Then, when it runs, it fetches the data and updates the state. Then, once the state is updated, the component re-renders, which triggers the useEffect again.
The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the 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>
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With