background
It all started because of the following code, and even discussed with my friends for a long time. You can take a look first, and we will talk about it in detail later, and poke 👉 codesandbox
import React, { useState, useEffect } from 'react'
function Article({ id }) {
const [article, setArticle] = useState(null)
useEffect(() = > {
let didCancel = false
console.log('effect', didCancel)
async function fetchData() {
console.log('setArticle begin', didCancel)
new Promise((resolve) = > {
setTimeout(() = > {
resolve(id)
}, id);
}).then(article= > {
// Quickly click the button on Add ID. Why is true printed here
console.log('setArticle end', didCancel, article)
// if (! DidCancel) {// Commenting out this line of code causes an error overwriting the status value
setArticle(article)
// }})}console.log('fetchData begin', didCancel)
fetchData()
console.log('fetchData end', didCancel)
return () = > {
didCancel = true
console.log('clear', didCancel)
}
}, [id])
return <div>{article}</div>
}
function App() {
const [id, setId] = useState(5000)
function handleClick() {
setId(id-1000)}return (
<>
<button onClick={handleClick}>add id</button>
<Article id={id}/>
</>
);
}
export default App;
Copy the code
The key code in useEffect is to modify the value of didCancel by clearing the side effect function, and then determine whether to perform setState immediately based on the value of didCancel, essentially to resolve the race situation.
A race is an error overwriting the state value in code that is mixed async/await and top-down data flow (the props and state may change during an async function call)
For example, in the above example, after we quickly click the button twice, we will see 3000 first and then see the result of 4000 on the page. This is because the state of 4000 is executed first, but returned later, so it will overwrite the last state, so we finally see 4000
UseEffect Clears the side effect function
We know that if we return a function in useEffect, that function is the cleanup function, and it is executed when the component is destroyed, but in fact, it is executed every time the component is re-rendered, removing the side effects of the previous effect.
A side effect is when a function does something that has nothing to do with the return value of its operation, such as changing a global variable, changing an argument passed in, or even console.log(), so Ajax operations, DOM changes, timers, and other asynchronous operations are all side effects
Consider the following code:
useEffect(() = > {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () = > {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
Copy the code
If the first render is {id: 10} and the second render is {id: 20}. Here’s what you might think happened:
- The React to clear the
{id: 10}
The effect of - The React to render
{id: 20}
The UI - Run the React
{id: 20}
The effect of
(It doesn’t.)
React will only run Effects after the browser has drawn. This makes your application much smoother because most effects don’t block updates on the screen. Effect clearing is also delayed, and the previous Effect will be cleared after rerendering:
- The React to render
{id: 20}
The UI - Browser drawn, seen on the screen
{id: 20}
The UI - The React to clear
{id: 10}
The effect of - Run the React
{id: 20}
The effect of
If clearing the previous effect happened after the props became {id: 20}, why did it still get the old {id: 10}?
Because each function within the component (including event handlers, effects, timers, or API calls, and so on) captures props and state in the render in which they were defined
So, an effect clearance does not read the latest props, it only reads the props value in the render in which it was defined
Analysis of the original 🌰
Analysis of the
Going back to our original example, let go of the uncommented code, and you have the following analysis.
After the first rendering
function Article() {... useEffect(() = > {
let didCancel = false
async function fetchData() {
new Promise((resolve) = > {
setTimeout(() = > {
resolve(id)
}, id);
}).then(article= > {
if(! didCancel) { setArticle(article) } }) } fetchData() }, [5000])
return () = > {
// Clear this render side effect, number it NO1, there is a hidden message, in this function, didCancel = false before execution
didCancel = true}}// Wait 5s, the page displays 5000,
Copy the code
By clicking a breakpoint on console.log(‘setArticle end’, didCancel, article), we can visually analyze the next operation 👉 by clicking the button twice quickly
/** On the first click, after the page is drawn, useEffect first executes the previous cleanup function, NO1, which sets didCancel in the last effect closure to true */
function Article() {... useEffect(() = > {
let didCancel = false
async function fetchData() {
new Promise((resolve) = > {
setTimeout(() = > { // setTimeout1
resolve(id)
}, id);
}).then(article= > {
if(! didCancel) { setArticle(article) } }) } fetchData() }, [4000])
return () = > {
// Clear this render side effect, number it NO2, there is a hidden message, didCancel = false in scope of this function
didCancel = true}}Copy the code
As you can see from DevTools:
/** Second click, after the page is drawn, useEffect first executes the previous cleanup function, NO2, which sets didCancel in the last effect closure to true */
function Article() {... useEffect(() = > {
let didCancel = false
async function fetchData() {
new Promise((resolve) = > {
setTimeout(() = > { // setTimeout2
resolve(id)
}, id);
}).then(article= > {
if(! didCancel) { setArticle(article) } }) } fetchData() }, [3000])
return () = > {
// Clear this render side effect, number it NO3, there is a hidden message, didCancel = false in scope of this function
didCancel = true}}Copy the code
As you can see from DevTools:
conclusion
After the second click, setTimeout2 is done first, didCancel is false, so setArticle is done, and the page shows 3000, why is didCancel false here, because NO2 is not doing clear, It is executed the next time the component is re-rendered, or when the component is unloaded.
After about 1s more, setTimeout2 completes, at which point didCancel is set to true by NO2’s clear function, so it will not perform the setArticle operation. So you don’t see 4000 and then 3000.
UseEffect Specifies how data is requested
Get data with async/await
// If you want to request initialization data while the component is hanging, you might use the following syntax
function App() {
const [data, setData] = useState()
useEffect(async() = > {const result = await axios('/api/getData')
setData(result.data)
})
}
Copy the code
However, we will notice that there is a warning message in the console:
We cannot use async directly in useEffect because the async function declaration defines an asynchronous function that returns an implicit Promise by default. However, in Effect Hook we should return nothing or a clear function. So we could do the following
function App() {
const [data, setData] = useState()
useEffect(() = > {
const fetchData = async() = > {const result = await axios(
'/api/getData',); setData(result.data); }; fetchData(); })}Copy the code
Tell React exactly what your dependencies are
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() = > {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={()= > setCounter(counter + 1)}>Increment</button>
</h1>
);
}
Copy the code
Effect hook will be executed every time we click on button to make counter+1. This is not necessary. We can add name to the dependency array of Effect, which tells React that when my name changes, You help me execute the function in effect.
If we add values from all of the components used in effects to our dependencies, sometimes the results are not very good. Such as:
useEffect(() = > {
const id = setInterval(() = > {
setCount(count+1)},1000)
return () = > clearInterval(id)
}, [count])
Copy the code
Although effect execution is triggered each time the count changes, the timer is recreated each time it is executed, which is not optimal. We added the count dependency because we use count in the setCount call, but we don’t use count anywhere else, so we can change the setCount call to a function form, so that setCount gets the current count value every time the timer is updated. So in the effect dependent array, we can kick count
useEffect(() = > {
const id = setInterval(() = > {
setCount(count= > count+1)},1000)
return () = > clearInterval(id)
}, [])
Copy the code
Decouple updates from Actions
Let’s modify the above example to include two states: count and step
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() = > {
const id = setInterval(() = > {
setCount(c= > c + step);
}, 1000);
return () = > clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e= > setStep(Number(e.target.value))} />
</>
);
}
Copy the code
At this point, modifying step will restart the timer because it is one of the dependencies. If we don’t want to restart the timer after a step change, how can we remove the dependency on step from Effect?
When you want to update a state that depends on another state, in this case count depends on step, you can replace them with useReducer
function Counter() {
const [state, dispatch] = useReducer(reducer, initState)
const { count, step } = state
const initState = {
count: 0.step: 1
}
function reducer(state, action) {
const { count, step } = state
switch (action.type) {
case 'tick':
return { count: count + step, step }
case 'step':
return { count, step: action.step }
default:
throw new Error()
}
}
useEffect(() = > {
const id = setInterval(() = > {
dispatch({ type: 'tick'})},1000);
return () = > clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e= > setStep(Number(e.target.value))} />
</>
);
}
Copy the code
Making Dispatch an Effect dependency in the code above will not trigger the execution of effect every time, because React guarantees that the Dispatch will remain the same for the duration of the component’s declaration cycle, so the timer will not be recreated.
You can remove the dispatch, setState, and useRef package values from dependencies because React ensures that they are static
Instead of reading the state directly in effect, it dispatches an action to describe what happened, which decouples our effect and step states. Our Effect no longer cares about updating the state, it just tells us what’s going on. All the updated logic was sent to reducer for unified processing
When you dispatch, React just remembers the action, and it will call the Reducer again on the next render, so the Reducer has access to the latest props in the component
conclusion
The purpose of this article is to help you re-understand useEffect and pay attention to the points of requesting data in useEffect. Please refer to 👇 for more details. If there are any errors in the above content, please feel free to point out.
Refer to the link
Overreacted. IO/useful – Hans/a – c…