I've been learning React and I read that the function returned from useEffect
is meant to do cleanup and React performs the cleanup when the component unmounts.
So I experimented with it a bit but found in the following example that the function was called every time the component re-renders as opposed to only the time it got unmounted from the DOM, i.e. it console.log("unmount");
every time the component re-renders.
Why is that?
function Something({ setShow }) { const [array, setArray] = useState([]); const myRef = useRef(null); useEffect(() => { const id = setInterval(() => { setArray(array.concat("hello")); }, 3000); myRef.current = id; return () => { console.log("unmount"); clearInterval(myRef.current); }; }, [array]); const unmount = () => { setShow(false); }; return ( <div> {array.map((item, index) => { return ( <p key={index}> {Array(index + 1) .fill(item) .join("")} </p> ); })} <button onClick={() => unmount()}>close</button> </div> ); } function App() { const [show, setShow] = useState(true); return show ? <Something setShow={setShow} /> : null; }
Live example: https://codesandbox.io/s/vigilant-leavitt-z1jd2
The useEffect Hook Usages. The callback function we pass to the useEffect hook runs the side effects. React runs it on every render of a component by default.
Effect cleanup functionsuseEffect(() => { // This is the effect itself. return () => { // This is its cleanup. }; }); Until React 17, the useEffect cleanup mechanism used to run during commit phase. This implies that when a component is unmounting, React would execute the cleanup functions and then update the screen.
The useEffect Hook is called every time the component renders. It works similarly to componentDidMount and componentDidUpdate in React class components.
React performs the cleanup when the component unmounts.
I'm not sure where you read this but this statement is incorrect. React performs the cleanup when the dependencies to that hook changes and the effect hook needs to run again with new values. This behaviour is intentional to maintain the reactivity of the view to changing data. Going off the official example, let's say an app subscribes to status updates from a friends' profile. Being the great friend you are, you are decide to unfriend them and befriend someone else. Now the app needs to unsubscribe from the previous friend's status updates and listen to updates from your new friend. This is natural and easy to achieve with the way useEffect
works.
useEffect(() => { chatAPI.subscribe(props.friend.id); return () => chatAPI.unsubscribe(props.friend.id); }, [ props.friend.id ])
By including the friend id in the dependency list, we can indicate that the hook needs to run only when the friend id changes.
In your example you have specified the array
in the dependency list and you are changing the array at a set interval. Every time you change the array, the hook reruns.
You can achieve the correct functionality simply by removing the array from the dependency list and using the callback version of the setState
hook. The callback version always operates on the previous version of the state, so there is no need to refresh the hook every time the array changes.
useEffect(() => { const id = setInterval(() => setArray(array => [ ...array, "hello" ]), 3000); return () => { console.log("unmount"); clearInterval(id); }; }, []);
Some additional feedback would be to use the id directly in clearInterval
as the value is closed upon (captured) when you create the cleanup function. There is no need to save it to a ref.
The React docs have an explanation section exactly on this.
In short, the reason is because such design protects against stale data and update bugs.
The useEffect
hook in React is designed to handle both the initial render and any subsequent renders (here's more about it).
Effects are controlled via their dependencies, not by the lifecycle of the component that uses them.
Anytime dependencies of an effect change, useEffect
will cleanup the previous effect and run the new effect.
Such design is more predictable - each render has its own independent (pure) behavioral effect. This makes sure that the UI always shows the correct data (since the UI in React's mental model is a screenshot of the state for a particular render).
The way we control effects is through their dependencies.
To prevent cleanup from running on every render, we just have to not change the dependencies of the effect.
In your case concretely, the cleanup is happening because array
is changing, i.e. Object.is(oldArray, newArray) === false
useEffect(() => { // ... }, [array]); // ^^^^^ you're changing the dependency of the effect
You're causing this change with the following line:
useEffect(() => { const id = setInterval(() => { setArray(array.concat("hello")); // <-- changing the array changes the effect dep }, 3000); myRef.current = id; return () => { clearInterval(myRef.current); }; }, [array]); // <-- the array is the effect dep
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