Hi,
I've a really simple component whose only purpose is to render an error message that I manage with redux. My thought was to return a function in the useEffect that clears the error message (something like dispatch(clearMessage)) because, to my knowledge, that function should run when the component is unmounted, which should occur only when navigating to another component.
I want to clear the message when unmounting in order to avoid the user going to the /error route by, for example, a redirect if there's no message.
So, I have another component that fetches data using react-query and, in case of error, use the dispatch to set the error message and then redirects the user to the error component.
The thing is, when I'm redirected to the error component, I see unmounting in the console immediately (which also runs clearMessage when I try to implement my idea, so it doesn't work as I expected). I don't get why, I guess there's some concept I'm missing.
So, what am I doing wrong?
import { useSelector } from 'react-redux'import { selectError } from '../feature/error/errorSlice'import { useEffect } from 'react'const Error = () => {const message = useSelector(selectError)useEffect(() => {return () => {console.log('unmounting')}}, [])return (<h2>{message}</h2>)}export default Error
1. Why is the return a function? return () => { ignore = true };
From the docs:
Why did we return a function from our effect? This is the optional cleanup mechanism for effects. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. They’re part of the same effect!
and
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. We’ll discuss why this helps avoid bugs and how to opt out of this behaviour in case it creates performance issues later below.
2. What is ignored used for in this example?
Initially in useEffect Hook ignore is set like, let ignore = false;.
When fetchProduct function executes it checks for ignore is true and accordingly sets setProduct(json).
This means we have state called product and setting the value in state using setProduct(json).
This product in state is used to render details on page.
Note: As [productId] is passed as second argument to useEffect, fetchProduct function will only get executes when productId changes.
See optimizing performance by skipping effects.
I will explain it here, as it took me a while to understand the explanations above, so I will try to make it simpler for others. Answers to your questions:
Why is return a function?
return () => { ignore = true };
useEffect offers the return function which is used for cleanup purposes. OK!, When do you need a cleanup? If You subscribe to something (like an event listener) and want to unsubscribe from it, this is where You should add the unsubscription logic to prevent a race condition or other issues!
What is ignore used for in this example?
ignore is used as a flag that tells your effect to ignore, or not, the API call. When is that used? When a race condition might occur.
Example of a race condition:
Imagine You have a list of products, and You fetch product details when a product is clicked. If You click on products quickly, multiple API calls might be in progress simultaneously, resulting in changes to product_id that would trigger useEffect to run multiple times. Here's what could happen:
- Click on
product1: an API call forproduct1will trigger. - Quickly click on
product2: triggers API call forproduct2. - API call for
product2finishes first and the page updates withproduct2data - API call for
product1finishes later, and the page updates withproduct1data.
Now data for product1 is displayed on the page, even though product2 was clicked last. This is a race condition.
BUT WHAT DOES THAT HAVE TO DO WITH ignore??
Logic explanation
ignore, my dear, tells setProduct to ignore the data from product1, and to not update the state with It, or in other words, It is used to ensure outdated API calls don't update the state when a race condition occurs.
Code explanation
First scenario : No race condition
- Click on
product1:ignore = false- call API for
product1 - Data arrives
If (!ignore) = true-> update state withproduct1data- Set
ignore = trueinside cleanup function. - Congrats!
product1data is displayed correctly.
Second scenario: Race condition
product1is clickedignore = false- call API for
product1 - data still hasn't arrived
- Quickly click on
product2ignore = false- call API for
product2
- Data for
product2arrives first!ignoreistrue-> update state withproduct2data- Cleanup function sets
ignore = true
- Data for
product1arrives laterif (!ignore) = false(becauseignore = true)- Oops, old data is ignored, so
product1data does not update the state.
And this is how we will always have up-to-date data.
Return statement in useEffect - javascript
In React, does a return statement return immediately or does it wait for the UseEffect to finish?
reactjs - Why can't useEffect access my state variable in a return statement? - Stack Overflow
Returning empty object in useEffect
Videos
Your understanding of the sequence of events is correct. The only thing missing is the precise timing of the effect callbacks and cleanup.
When the component re-renders, any useEffects will have their dependency arrays analyzed for changes. If there has been a change, then that effect callback will run. These callbacks are guaranteed to run in the order that they're declared in the component. For example, below, a will always be logged just before b.
const App = () => {
const [num, setNum] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setNum(num => num + 1);
}, 1000);
}, []);
React.useEffect(() => {
console.log('a', num);
}, [num]);
React.useEffect(() => {
console.log('b', num);
}, [num]);
return num;
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>
These effect callbacks will run shortly after the browser re-paints.
Now add the effect cleanup callback into the mix. These will always run synchronously just before the effect callback for a render runs. For example, let's say the component starts at Render A, and in Render A, an effect hook has returned a cleanup callback. Then, some state changes, and a transition to Render B occurs, and there exists a useEffect with a dependency array that includes the state change. What will happen is:
- The functional component will be called with the new props/state, for Render B
- The component returns the new markup at the end of the function
- The browser repaints the screen if necessary
- The cleanup function from render A will run
- The effect callback from render B will run
You can see the source code for those last two actions here:
commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);
That first call invokes all cleanup callbacks from a prior render. That second call invokes all effect callbacks for the current render. Current render effect callbacks run synchronously after the execution of prior render cleanup callbacks.
Does a functional component 'dismount' after a change in its state and does this result in previous useEffect callback return statements executing?
No, component dismounts only once at end of its life time, React allows to execute a callback with useEffect hook by providing an empty dep array with a return statement:
useEffect(() => {
return () => {
console.log("unmounts");
};
}, []);
When component dismounts?
When its parent stops rendering it. See conditional rendering.
Could you help me understand, in general or in the example in my question, when the return statement of useEffect executes?
Depends on the dep array:
- if it's empty
[], on unmount. - if it has dependencies
[value1,value2], on dependencies change (shallow comparison). - if it has no dependencies (no 2nd argument for
useEffect) it runs on every render.
See follow up question useEffect in depth / use of useEffect?
I think it is a typical stale closure problem. And it is hard to understand at first.
With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also. You can resolve the problem more than one way.
One of them is to add the state variable to your dependency.
useEffect(() => {
return () => logAbandonListing()
}, [progress])
Another solution is that you set the state value to a ref. And the reference of the ref is not changing, so you will always see the freshest value.
let[progress, setProgress] = React.useState(0);
const progressRef = React.createRef();
progressRef.current = progress;
...
const logAbandonListing = () => {
console.log(`progress inside: ${progressRef.current}`)
if (progressRef.current > 0) {
addToLog(userId)
}
}
If userId is changing too, then you should add it to the dependency or a reference.
To do something in the state's current value in the useEffect's return function where the useEffects dependencies are am empty array [], you could use useReducer. This way you can avoid the stale closure issue and update the state from the useReducer's dispatch function.
Example would be:
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
More info on this answer
In the below code what is the purpose of return ()=>{}
useEffect(() => {
try{ var verified = jwt.verify(cookies['token'], props.privateKey); setUsername(verified.username);setBalance(verified.balance);}
catch (err)
{setUsername(""); setBalance(0);}
return () => { } ==>>> ????
}, [cookies, props.privateKey])