A summary of some pitfalls you will encounter when using useEffect and asynchronous tasks.
Three common questions:
1. How to initiate asynchronous tasks when component loads
2. How to initiate asynchronous tasks during component interaction
3. Other traps
1. React hooks send asynchronous requests
1. Use useEffect to initiate an asynchronous task. The second parameter uses an empty array to implement the method body when the component is loaded. The return value function is executed once when the component is unloaded to clean up some things.
2. Use AbortController or the semaphore ( ) provided by some libraries to control the abort request and exit more gracefully
3. When you need to set a timer in other places (such as in the click processing function) and clean up in the useEffect return value, use local variables or useRef to record this timer, and do not use useState.
4. When closures such as setTimeout appear in the component, try to refer to ref instead of state inside the closure, otherwise it is easy to read old values.
5. The update status method returned by useState is asynchronous, and a new value must be obtained next time you repaint. Don't try to get the status immediately after changing it.
2. How to initiate asynchronous tasks when component loads
This type of requirement is very common. A typical example is to send a request to the backend when the list component is loaded to get the list display.
import React, { useState, useEffect } from 'react'; const SOME_API = '/api/get/value'; export const MyComponent: <{}> = => { const [loading, setLoading] = useState(true); const [value, setValue] = useState(0); useEffect( => { (async => { const res = await fetch(SOME_API); const data = await ; setValue(); setLoading(false); }); }, []); return ( <> { loading ? ( <h2>Loading...</h2> ) : ( <h2>value is {value}</h2> ) } </> ); }
As mentioned above, a basic component with Loading function will send an asynchronous request to the backend to obtain a value and display it on the page.
If it is sufficient by example standards, but it should be applied to the project,I have to consider a few issues
3. What happens if the component is destroyed before the response comes back?
At this time, React will report a ⚠️ warning message:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subions and asynchronous tasks in a useEffect cleanup Notification
That is to say, a component should not be modified after it is uninstalled. Although it does not affect operation, this problem should not exist. So how to solve it?
The core of the problem is that after the component is uninstalled, setValue() and setLoading(false) are still called to change the state.
Therefore, an easy way is to mark whether the component has been uninstalled, and you can use the return value of useEffect.
//Omit other contents of the component and list only diffuseEffect( => { let isUnmounted = false; (async => { const res = await fetch(SOME_API); const data = await ; if (!isUnmounted) { setValue(); setLoading(false); } }); return => { isUnmounted = true; } }, []);
This will prevent this Warning smoothly.
Is there a more elegant solution?
The above approach is to make a judgment when a response is received, that is, it is a bit passive when the response is completed no matter what.
A more proactive way is to detect that the request is directly interrupted during uninstallation, so naturally there is no need to wait for the response.
This proactive solution requires the use of AbortController.
AbortControllerIt is an experimental interface for a browser, it canReturns a semaphore (singal), thereby aborting the sent request.
This interface has good compatibility, except IE (such as Chrome, Edge, FF and most mobile browsers, including Safari).
useEffect( => { let isUnmounted = false; const abortController = new AbortController; // Create (async => { const res = await fetch(SOME_API, { singal: , // As a semaphore }); const data = await ; if (!isUnmounted) { setValue(); setLoading(false); } }); return => { isUnmounted = true; ; // Interrupted during component uninstallation } }, []);
The implementation of singal relies on the method used to actually send requests, the fetch method as shown in the above example accepts the single attribute.
If you are using axios, it already contains it and can be used directly, an example is here.
import React, { Component } from 'react'; import axios from 'axios'; class Example extends Component { signal = (); state = { isLoading: false, user: {}, } componentDidMount() { (); } componentWillUnmount() { ('Api is being canceled'); } onLoadUser = async () => { try { ({ isLoading: true }); const response = await ('/api/', { cancelToken: , }) ({ user: , isLoading: true }); } catch (err) { if ((err)) { ('Error: ', ); // => prints: Api is being canceled } else { ({ isLoading: false }); } } } render() { return ( <div> <pre>{(, null, 2)}</pre> </div> ) } }
4. How to initiate asynchronous tasks during component interaction
Another common requirement isSend a request or turn on the timer when the component interacts (such as clicking a button), modify the data after receiving the response and then affect the page。
The biggest difference here and the above section (when component loading) is that React Hooks can only be written at the component level, not at the method (dealClick
) or control logic (if
, for
etc.) written internally, so you cannot call it in the response function of the clickuseEffect
. But we still need to use ituseEffect
return function to clean up.
Take the timer as an example. Suppose we want to make a component, and turn on a timer (5s) after clicking the button, and modify the status after the timer is finished.
But if the component is destroyed before the timer is reached, we want to stop this timer to avoid memory leakage. If implemented in code, you will find that turning on the timer and cleaning the timer will be in different places, so you must record this timer.
See the following example:
import React, { useState, useEffect } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); let timer: number; useEffect(() => { // timer needs to be created when clicked, so only cleanup is used here return () => { ('in useEffect return', timer); // <- Correct value (timer); } }, []); function dealClick() { timer = (() => { setValue(100); }, 5000); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me!</button> </> ); }
Since you want to record the timer, you can naturally use an internal variable to store it (not considering that clicking the button continuously causes multiple times to appear, assuming that you only click once. Because clicking the button in actual situations will trigger other state changes, and then the interface changes, so you can't click it).
Need hereNoticeYes,Iftimer
Upgrade to state, the code will insteadProblems arise。
Consider the following code:
import React, { useState, useEffect } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); const [timer, setTimer] = useState(0); // Upgrade timer to state useEffect(() => { // timer needs to be created when clicked, so only cleanup is used here return () => { ('in useEffect return', timer); // <- 0 (timer); } }, []); function dealClick() { let tmp = (() => { setValue(100); }, 5000); setTimer(tmp); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me!</button> </> ); }
Semanticallytimer
Let’s put aside whether it is considered a component’s state, just look at the code level.
useuseState
Come and remembertimer
Status, usesetTimer
It seems reasonable to change the status.
But actually running,useEffect
The returned cleanup function is obtainedtimer
It is the initial value, i.e.0
。
Why are there any differences between the two writing methods?
The core of it lies in whether the variable written and the variable read are the same variable.
The first way to write: The code istimer
Used as a local variable within a component. When the component is rendered for the first time,useEffect
The returned closure function points to this local variabletimer
. existdealClick
When setting the timer, the return value is still written to this local variable (that is, both read and write are the same variable), so during subsequent uninstallation, although the component reruns, a new local variable appearstimer
, but this does not affect the old ones in the closuretimer
, so the result is correct.
The second way to write: timer
It's oneuseState
The return value of , is not a simple variable. Judging from the source code of React Hooks, it returns[, dispatch]
, corresponding to the value we are connected to and change method. When calledsetTimer
andsetValue
When the repaint is triggered twice respectively, so thatPointed to
newState
(Note: not modify, but repoint). butuseEffect
Return to the closuretimer
Still pointing to the old state, so that the new value cannot be obtained. (That is, the old value is read, but the new value is written, not the same)
If you feel it is difficult to read the source code of Hooks, you can understand it from another perspective: Although React launched Hooks in 16.8, it actually only strengthened the writing of functional components to have states and used them as a substitute for class components, but the internal mechanism of React state has not changed. In ReactsetState
Inside, after combining the new state and the old state through the merge operation, return to a new state object. No matter how Hooks are written, this principle has not changed. Now the closure points to the old state object,setTimer
andsetValue
Regenerating and pointing to a new state object does not affect the closure, causing the closure to not read the new state.
We noticed that React also provides us with oneuseRef
, its definition is:
- useRef Returns a mutable ref object with
current
The attribute is initialized as an incoming parameter (initialValue). - The returned ref objectStay unchanged throughout the life of the component.
The ref object ensures that the value remains unchanged throughout the life cycle and is updated synchronously because the return value of ref always has only one instance, and all reads and writes point to itself. So it can also be used to solve the problem here.
import React, { useState, useEffect, useRef } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); const timer = useRef(0); useEffect(() => { // timer needs to be created when clicked, so only cleanup is used here return () => { (); } }, []); function dealClick() { = (() => { setValue(100); }, 5000); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me!</button> </> ); }
In fact, we will see later.useRef
It is safer and safer to cooperate with asynchronous tasks.
V. Other traps
Modification status is asynchronous
//Error Exampleimport React, { useState } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); function dealClick() { setValue(100); (value); // <- 0 } return ( <span>Value is {value}, AnotherValue is {anotherValue}</span> ); }
useState
The returned modification function is asynchronous and will not take effect directly after being called, so it is read immediately.value
What is obtained is the old value (0
)。
The purpose of React is designed for performance considerations. It is easy to understand that all states are changed and redrawn only once can solve the update problem, rather than changing and redrawing once.
No new value for other states can be read in timeout
//Error Exampleimport React, { useState, useEffect } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); const [anotherValue, setAnotherValue] = useState(0); useEffect(() => { (() => { ('setAnotherValue', value) // <- 0 setAnotherValue(value); }, 1000); setValue(100); }, []); return ( <span>Value is {value}, AnotherValue is {anotherValue}</span> ); }
This question is used aboveuseState
Go to recordtimer
Similarly, when generating a timeout closure, the value of value is 0.
Although afterwardsetValue
The state has been modified, but React has pointed to the new variable inside, and the old variable is still referenced by the closure, so the closure still gets the old initial value, that is, 0.
To fix this problem, it is still useduseRef
,as follows:
import React, { useState, useEffect, useRef } from 'react'; export const MyComponent: <{}> = () => { const [value, setValue] = useState(0); const [anotherValue, setAnotherValue] = useState(0); const valueRef = useRef(value); = value; useEffect(() => { (() => { ('setAnotherValue', ) // <- 100 setAnotherValue(); }, 1000); setValue(100); }, []); return ( <span>Value is {value}, AnotherValue is {anotherValue}</span> ); }
Or timeout problem
Suppose we want to implement a button, which displays false by default. Change to true after clicking, but then return to false after two seconds (true and false are interchangeable).
Consider the following code:
import React, { useState } from 'react'; export const MyComponent: <{}> = () => { const [flag, setFlag] = useState(false); function dealClick() { setFlag(!flag); setTimeout(() => { setFlag(!flag); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }
We will find that we can switch normally when clicking, but it will not change back in two seconds.
The reason is stilluseState
The update is to repoint the new value, but the closure of timeout still points to the old value.
So in the example,flag
Alwaysfalse
, although subsequentsetFlag(!flag)
, but it still does not affect the timeoutflag
。
There are two solutions.
The first one is to use ituseRef
import React, { useState, useRef } from 'react'; export const MyComponent: <{}> = () => { const [flag, setFlag] = useState(false); const flagRef = useRef(flag); = flag; function dealClick() { setFlag(!); setTimeout(() => { setFlag(!); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }
The second is to usesetFlag
Can accept functions as parameters and use closures and parameters to implement (functional update)
import React, { useState } from 'react'; export const MyComponent: <{}> = () => { const [flag, setFlag] = useState(false); function dealClick() { setFlag(!flag); setTimeout(() => { setFlag(flag => !flag); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }
whensetFlag
When the parameter is a function type, the meaning of this function is to tell React how toCurrent statusGenerateA new state(Similar to the reducer of redux, but only for one state child reducer). Since it is the current state, the effect can be achieved by inversely replacing the return value.
Summarize
When asynchronous tasks appear in Hook, especially timeout, we must pay special attention.
useState
Only guarantee the state between multiple repaintsvalueIt's the same, but it's not guaranteed that they are the same object.Therefore, when a closure reference occurs, try to use it as much as possibleuseRef
Instead of using the state itself directly, otherwise it will be easy to get stuck.
On the contrary, if you do encounter a situation where a new value is set but the old value is read, you can also think about it in this direction, which may be due to this reason.
The above is personal experience. I hope you can give you a reference and I hope you can support me more.