Read the Facebook boss: Dan Abramov got a lot of insight
Big guy github address github.com/gaearon
The key summary
useEffect
Is synchronous- The state is the current capture
props
和state
- Can be achieved by
useRef
Get the changedprops
和state
- dependency
[]
Can’t cheat - Complex state changes should be used
useReducer
- You can use
useCallback
Set the dependence - You can use
useMemo
Let complex objects change dynamically
But sometimes when you use the useEffect you feel something is wrong. You mutter that you might be missing something. It looks like the life cycle of class… But is it really true? You find yourself asking questions like:
- 🤔 how to use
useEffect
simulationcomponentDidMount
Life cycle? - How to 🤔 correctly
useEffect
Request data in?[]
What is it? - 🤔 Should I treat a function as an effect dependency?
- 🤔 Why do endless repeated requests sometimes occur?
- 🤔 Why do I sometimes get old State or prop in Effect?
When I stop peeking through the familiar class lifecycle approachuseEffect
This Hook, I was able to understand.
“Forget what you’ve learned.” – Yoda
Abstract
If you’re going to read the whole article, you can skip this part altogether. I’ll include a link to the abstract at the end of the article.
🤔 Question: How to use ituseEffect
simulationcomponentDidMount
Life cycle?
UseEffect (fn, []) can be used, but they are not exactly equal. Unlike componentDidMount, useEffect captures props and state. So even in the callback function, you still get the original props and state. If you want the “latest” value, you can use ref. However, there are usually simpler implementations, so you don’t have to use ref. Keep in mind that the mental model of Effects is different from componentDidMount and other life cycles, and trying to find exactly the same expression between them is more likely to confuse you. To be more effective, you need to “think in Effects “, whose mental model is closer to achieving state synchronization than to responding to life cycle events.
🤔 Question: How to correct inuseEffect
Request data in?[]
What is it?
This article is a good introduction to how to make data requests in useEffect. Be sure to read it! It’s not as long as mine. [] indicates that effect does not use any values in the React stream, so it is safe to call effect only once. [] is also the source of a common problem where you think you are not using a value in the data stream but you are. You need to learn some strategies (mainly useReducer and useCallback) to remove these effect dependencies rather than mistakenly ignoring them.
🤔 Question: Should I treat a function as an effect dependency?
It is generally recommended that functions that do not depend on props and state be outside of your components, and functions that are only used by effect be inside effect. If, after doing so, you still need to use functions inside the component (including functions passed in through props), you can use a useCallback layer where you define them. Why do you do that? Because these functions have access to props and state, they participate in the data flow. The FAQ on our website has more detailed answers.
🤔 Question: Why do you sometimes have an infinite number of repeated requests?
This usually happens when you make a data request in Effect without setting an effect dependent parameter. Without setting dependencies, effect is executed once after each render, and then updates the state in effect to cause the render and trigger effect again. Infinite loops can also happen because the dependencies you set are always changing. You can figure out which dependencies are causing the problem by removing them one by one. However, removing the dependencies you use (or blindly using []) is often the wrong solution. What you should do is tackle the root cause of the problem. For example, functions can cause this problem. You can place them in effects, or outside components, or wrap them in useCallback. UseMemo can do something similar to avoid generating objects repeatedly.
🤔 Why do I sometimes get old State or prop in Effect?
Effect always gets props and state from the render that defined it. This can avoid some bugs, but can be annoying in some scenarios. For these scenarios, you can explicitly store some values using mutable refs (explained at the end of the article above). If you feel like you’re getting old props and states in a render that you didn’t want, you’re probably missing some dependencies. Try using the Lint rule to train you to find these dependencies. After a few days, it might seem like second nature. You can also see this answer in our FAQ.
I hope this summary is helpful! Or, let’s start the text.
Each render has its own Props and State
Before we talk about Effects, we need to talk about rendering.
Let’s look at a Counter component:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times </p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
What does highlighted code mean? Does count “listen” for changes in status and update automatically? This may be a useful first instinct for learning React, but it’s not an accurate mental model.
** In the example above, count is just a number. ** It’s not some magical “data Binding “, “Watcher “, “proxy”, or anything else. It’s just a regular number like this:
const count = 42;
<p>You clicked {count} times </p>
Copy the code
When our component first renders, it gets the initial count value of 0 from useState(). When we call setCount(1), React will render the component again, this time with count 1. And so on:
function Counter() {
const count = 0;
<p>You clicked {count} times</p>
}
function Counter() {
const count = 1;
<p>You clicked {count} times</p>
}
function Counter() {
const count = 2;
<p>You clicked {count} times</p>
}
Copy the code
React will re-render the component when we update the state. Each render gets its owncount
State, the value of which is a constant in the function.
So this line of code doesn’t do any special data binding:
<p>You clicked {count} times</p>
Copy the code
** It simply inserts the number count into the render output. ** This number is provided by React. When setCount is set, React calls the component again with a different count value. React then updates the DOM to keep it consistent with the rendered output.
The key point here is that the count constant in any render does not change over time. The render output changes because our component is called from time to time, and the render resulting from each call contains a count value independent of the other renderings.
(A more in-depth discussion of this process can be found in another of my articlesReact as a UI Runtime).
Each render has its own event handler
So far so good. What about event handlers?
Look at the example below. It will alert count of clicks after three seconds:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() { setTimeout((a)= > { alert('You clicked on: ' + count); }, 3000); }
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}
Copy the code
If I follow these steps:
- Click to increase counter to 3
- Click on “Show Alert”
- Click to increase counter to 5 and complete before the timer callback triggers
Try it out for yourself!
This article delves into why. The correct answer is 3. Alert “captures” the state when I click the button.
(There are other ways to implement different behaviors, but for now I’ll focus on this default scenario. When building a mental model, it is important to identify the “path of least resistance” among the alternative strategies.
But how exactly does it work?
We find that count is a constant value in every function call. It is worth emphasizing – our component function is called every time we render, but the count value is constant in each call, and it is given the state value in the current render.
This isn’t unique to React; ordinary functions behave similarly:
function sayHi(person) {
const name = person.name; setTimeout((a)= > {
alert('Hello, ' + name);
}, 3000);
}
let someone = {name: 'Dan'};
sayHi(someone);
someone = {name: 'Yuzhi'};
sayHi(someone);
someone = {name: 'Dominic'};
sayHi(someone);
Copy the code
In this example, the outer someone will be assigned multiple times (just as in React, the component state of _ currently _ will change). ** Then, in the sayHi function, the local constant name is associated with the person in the call. ** Because this constant is local, each call is independent of each other. As a result, each alert pops up its own name when the timer callback is triggered.
This explains how our event handler captures the count value at click time. If we apply the same substitution principle, each render “sees” its own count:
function Counter() {
const count = 0;
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + count);
}, 3000); }}function Counter() {
const count = 1;
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + count);
}, 3000); }}function Counter() {
const count = 2;
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + count);
}, 3000); }}Copy the code
So in effect, every rendering has a “new version” of handleAlertClick. Each version of handleAlertClick” remembers “its own count:
function Counter() {
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + 0); }, 3000);
}
<button onClick={handleAlertClick} />
}
function Counter() {
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + 1); }, 3000);
}
<button onClick={handleAlertClick} />
}
function Counter() {
function handleAlertClick() {
setTimeout((a)= > {
alert('You clicked on: ' + 2); }, 3000);
}
<button onClick={handleAlertClick} />
}
Copy the code
That’s why in this demo, the event handler “belongs” to a particular render, and when you click, it uses the state of counter from that render.
** During any render, props and state are always the same. ** If props and state are independent of each other in different renderings, then any values used for them are also independent (including event handlers). They all “belong” to a particular render. Even the asynchronous function call in event handling “sees” the count value in this render.
Note: ABOVE I will be specificcount
The values are directly inlinedhandleAlertClick
In a function. This mental substitution is safe becausecount
Values cannot be changed in a particular render. It’s declared as a constant and a number. It’s also safe to think about other types of values like objects, provided we all agree that we should avoid directly changing state. By calling thesetSomething(newObj)
It is better to generate a new object rather than modify it directly, as this ensures that the state in the previous rendering is not contaminated.
Each render has its own Effects
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > { document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times </p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
Here’s a question for you: How does Effect read up to datecount
What about the state values?
Perhaps there is some sort of “data binding” or “watching” mechanism that allows count to be updated within the effect function? Or maybe count is a mutable value and React changes it inside our component so that our effect function always gets the latest value?
Neither.
We already know that count is a constant in a particular render. The event handler “sees” the value of the count state that belongs to its particular render. The same is true for Effects:
Is notcount
The value of is changed in “unchanged” effect, insteadThe effect function itselfIt’s different in every render.
Each effect version “sees” the count value from the render it belongs to:
function Counter() {
useEffect(
(a)= > { document.title = `You clicked The ${0} times`; }); }function Counter() {
useEffect(
(a)= > { document.title = `You clicked The ${1} times`; }); }function Counter() {
useEffect(
(a)= > { document.title = `You clicked The ${2} times`; }); }Copy the code
React remembers the effect function you provided and will call it every time the change is applied to the DOM and the browser draws the screen.
So while we’re talking about an effect, each render is a different function — and each effect function “sees” props and state from the particular render to which it belongs.
Conceptually, you can imagine effects being part of the result of the render.
Strictly speaking, they are not (in order to allow Hook composition without introducing awkward syntax or runtime). But in the mental model we built, the effect function _ belongs to a particular render, just like the event handler.
To make sure we have a solid understanding, let’s review the first rendering process:
- React:Give me the status
0
Time UI. - Your component:
- Give you the content you need to render:
<span> You clicked 0 times</span>
. - Remember to call this effect after rendering:
() => { document.title = 'You clicked 0 times' }
.
- Give you the content you need to render:
- React: No problem. Start updating the UI, feed the browser, and I’m going to add something to the DOM.
- Browser: Cool, I’ve drawn it on the screen.
- React:Ok, I will now run the effect given to me
- run
() => { document.title = 'You clicked 0 times' }
.
- run
Now let’s review what happens after we click:
- Your component:Hey React, set my state to
1
. - React:Give me the status
1
Time UI. - Your component:
- Give you the content you need to render:
<span> You clicked 1 times</span>
. - Remember to call this effect after rendering:
() => { document.title = 'You clicked 1 times' }
.
- Give you the content you need to render:
- React: No problem. I started updating the UI, feeding the browser, and I modified the DOM.
- Browser: Cool, I’ve already drawn the changes to the screen.
- React:Ok, I will now run the effects that belong to this render
- run
() => { document.title = 'You clicked 1 times' }
.
- run
Every render has its own… all
We now know that Effects will run after each render, and that it is conceptually part of the component output and can “see” the props and states that belong to a particular render.
As a thought experiment, consider the following code:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > { setTimeout((a)= > { console.log(`You clicked ${count} times`); }, 3000); });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
What happens if I click multiple times and set the delay in Effect?
You might think this is a tricky problem and the result is counterintuitive. Totally wrong! What we see is a sequential printout – each one belongs to a particular render and therefore has the count value it should have. Try it for yourself:
However, this.state in class doesn’t work that way. You might assume that the following class implementation is equivalent to the above:
This.state. count always points to the latest count value, not the value that belongs to a particular render. So you’ll see that each printout is 5:
I find it ironic that Hooks rely so heavily on Javascript closures. Sometimes the component’s class implementation suffers from the canonical wrong-value-in-a-timeout confusion, But the real source of confusion in this example is mutable data (React modifies this.state in the class to point to the latest state), not the closure itself.
Closures are great when the closed value never changes. And that makes them very easy to think about because you’re essentially referencing constants. ** As we discussed, props and state do not change in a particular render. By the way, we can fix the above class version using closures…
tide
By now, we can explicitly shout out the important fact that functions within each component (including event handlers, effects, timers, or API calls, etc.) capture props and state defined in a particular render.
So the following two examples are equivalent:
function Example(props) {
useEffect((a)= > {
setTimeout((a)= > {
console.log(props.counter);
}, 1000);
});
}
Copy the code
function Example(props) {
const counter = props.counter;
useEffect((a)= > {
setTimeout((a)= > {
console.log(counter);
}, 1000);
});
}
Copy the code
It doesn’t matter when the props or state is read in the component. ** Because they don’t change. Throughout the scope of a single render, the props and state remain the same. (Deconstructing the props for assignment makes this more obvious.)
Of course, sometimes you might want to read the latest value in the effect callback instead of the captured value. The easiest way to do this is to use Refs, which is covered in the last section of this article.
Note that when you want to read future props and state from functions rendered in the past, you are swimming against the tide. While there is nothing wrong with it (and it may sometimes be necessary to do so), it makes the code look less “clean” by breaking the default paradigm. This is intentional because it helps highlight which code is vulnerable and time-dependent. In class, it’s not so obvious if this happens.
The following counter version emulates the behavior in class:
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect((a)= > {
latestCount.current = count;
setTimeout((a)= > {
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
}
Copy the code
Changing values directly in React looks a little weird. However, React does exactly that in the class component. Unlike captured props and state, you can’t guarantee that latestCount.current read in any callback function is constant. By definition, you can change it at any time. That’s why it’s not the default behavior, but something you actively choose to do.
What about cleaning in Effect?
As explained in the documentation, some Effects may require a cleanup step. Essentially, it aims to eliminate side effects, such as unsubscribing.
Consider the following code:
useEffect((a)= > {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return (a)= > {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
Copy the code
Suppose the props were {id: 10} for the first rendering and {id: 20} for the second rendering.
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. The previous effect will be cleared after rerendering:
- The React to render
{id: 20}
UI. - Browser drawing. We see it on the screen
{id: 20}
UI. - The React to clear
{id: 10}
The effect of. - Run the React
{id: 20}
The effect of.
You might be wondering: if clearing the previous effect happened after props became {id: 20}, why is it still “seeing” the old {id: 10}?
To quote the conclusion from the first half:
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.
Now the answer is obvious. The effect cleanup does not read the “latest” props. It can only read props for the render in which it was defined:
Synchronization, not life cycle
What I like most about React is that it uniformly describes the initial rendering and subsequent updates. This lowers the entropy of your program.
Let’s say I have a component that looks like this:
function Greeting({ name }) {
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
Copy the code
I first render
People always say, “It’s the journey, not the destination, that matters.” In the React world, the opposite is true. It’s the end that matters, not the process. ** This is the difference between calling (procedures) like $.addClass or $.removeclass in JQuery code and declaring what the CSS class name should be (purpose) in React code.
React will synchronize to DOM based on our current props and state. There is no difference between “mount” and “update” in rendering.
You should think of Effects the same way. ** useEffect allows you to synchronize things outside the React Tree based on props and state. **
function Greeting({ name }) {
useEffect((a)= > { document.title = 'Hello, ' + name; }); return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
Copy the code
This is a subtle difference from the familiar _mount/update/unmount_ mental model. It is important to understand and internalize this distinction. ** If you try to write an effect that behaves differently depending on whether it was first rendered, you are swimming against the tide. ** If our results depend on the process rather than the destination, we will make mistakes in synchronization.
Rendering attributes A, B and C is no different from rendering C immediately. While they may be slightly different for a short time (such as when requesting data), the end result is the same.
Then again, it might not be efficient to run all effects after every _ render. (And in some scenarios, it can cause an infinite loop.)
So how do we solve this problem?
Tell React to compare your Effects
We’ve already learned the solution from the way React handles the DOM. React only updates the parts of the DOM that actually change, rather than making a big deal out of every render.
When you take your
<h1 className="Greeting">
Hello, Dan
</h1>
Copy the code
Update to the
<h1 className="Greeting">
Hello, Yuzhi
</h1>
Copy the code
React can see two objects:
const oldProps = {className: 'Greeting'.children: 'Hello, Dan'};
const newProps = {className: 'Greeting'.children: 'Hello, Yuzhi'};
Copy the code
It checks each of the props and finds that the DOM needs to be updated if the children changes, but the className does not. So it just needs to do this:
domNode.innerText = 'Hello, Yuzhi';
Copy the code
Can we do effects in a similar way? It would be nice to be able to avoid calling effect when you don’t need it.
For example, our component might be re-rendered due to a state change:
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect((a)= > {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={()= > setCounter(counter + 1)}>Increment</button>
</h1>
);
}
Copy the code
However, our effect does not use the state counter. ** Our effect will only synchronize the name property to document.title, but the name will not change. ** Reassigning document.title after every counter change is not ideal.
Okay, React can… Does it distinguish between effects?
let oldEffect = (a)= > { document.title = 'Hello, Dan'; };
let newEffect = (a)= > { document.title = 'Hello, Dan'; };
Copy the code
And can’t. React can’t guess what the function is doing unless it’s called first. (The source code contains no special value, it simply refers to the name attribute.)
This is why if you want to avoid unnecessary repetition of effects calls, you can provide useEffect with a dependency array argument (deps) :
useEffect((a)= > {
document.title = 'Hello, ' + name;
}, [name]);
Copy the code
It’s like telling React: “Hey, I know you can’t see what’s in this function, but I can guarantee that only the render is usedname
And nothing else.”
React will automatically skip this effect if the dependencies in the current render have the same values as the last time the effect was run, since there is nothing to synchronize:
const oldEffect = (a)= > { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];
const newEffect = (a)= > { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];
Copy the code
Even if only one value in the dependency array is different between renderings, we cannot skip effect running. Sync all!
Don’t lie to React about dependencies
Lying to React about dependencies has bad consequences. Intuitively, this makes sense, but I’ve seen almost everyone who relies on the class mental model to use useEffect try to violate this rule. (I just did that!)
function SearchResults() {
async function fetchData() {
}
useEffect((a)= >{ fetchData(); } []); }Copy the code
(websiteHooks FAQExplained what to do. We are inThe followingI’ll revisit this example.)
“But I just want to run it while it’s mounted!” “, you might say. Now just remember: if you set up a dependency, all components used in **effect will be included in the dependency. ** This includes props, state, functions — anything inside the component.
Sometimes you do, but that can cause a problem. For example, you might have problems with infinite requests, or sockets being created frequently. ** The solution is not to remove dependencies. ** We will know the specific solution soon.
But before we dive into the solution, let’s try to understand the problem better.
What happens if you set the wrong dependency?
If the dependency contains all the values used in Effect, React knows when to run it:
useEffect((a)= > {
document.title = 'Hello, ' + name;
}, [name]);
Copy the code
(The dependency has changed, so Effect is rerun.)
But if we set [] to an effect dependency, the new effect function will not run:
useEffect((a)= > {
document.title = 'Hello, '+ name; } []);Copy the code
(The dependency has not changed, so Effect will not run again.)
In this example, the problem seems obvious. But in some cases if you “jump” out of your head with the class component solution, your intuition is likely to deceive you.
For example, let’s write a counter that increases every second. In the Class component, our intuition is: “Once on a timer, once off”. Here’s an example of how to do it. When we useEffect it as a matter of course, we intuitively set the dependency to []. I only want to run effect” once, right?
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= >clearInterval(id); } []);return <h1>{count}h1>;
}
Copy the code
However, this example will increment only once. Days and lulu.
If your mental model is “I only need to set dependencies when I want to re-trigger an effect”, this example might give you an existential crisis. You want to trigger once because it’s a timer — but why is that a problem?
You won’t be surprised if you know that dependencies are our cue to React, telling effect all the values in the render it needs to use. Effect uses count but we lie and say it has no dependency. If we do this sooner or later something will happen.
In the first rendering, count is 0. Therefore, setCount(count + 1) is equivalent to setCount(0 + 1) on the first rendering. Now that we have set the [] dependency, effect will not run again, and setCount(0 + 1) will be called every second after it:
function Counter() {
useEffect(
(a)= > {
const id = setInterval((a)= > {
setCount(0 + 1);
}, 1000);
return (a)= >clearInterval(id); } []); }function Counter() {
useEffect(
(a)= > {
const id = setInterval((a)= > {
setCount(1 + 1);
}, 1000);
return (a)= >clearInterval(id); } []); }Copy the code
We lied to React that our effect doesn’t depend on any values in the component, when in fact our effect does!
Our effect depends on count – it is the value inside the component (but defined outside effect) :
const count = / /...
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= >clearInterval(id); } []);Copy the code
Therefore, setting [] to dependency introduces a bug. React compares dependencies and skips effects:
(The dependency has not changed, so Effect will not run again.)
It’s hard to think of a problem like this. Therefore, I encourage you to make it a hard and fast rule to tell Effect dependencies honestly, and to list all dependencies. (We provide a Lint rule if you want to enforce it within your team.)
Two ways to be honest about dependencies
There are two honest dependency strategies. You should start with the first and apply the second as needed.
** The first strategy is to include values in all components used in effects in the dependency. ** Let’s include count in the dependency:
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= > clearInterval(id);
}, [count]);
Copy the code
The dependency array is now correct. It may not be ideal but it does solve the above problem. Effect is now re-run every time count is changed, and setCount(count + 1) in the timer refers to the count value in the render correctly:
function Counter() {
useEffect(
(a)= > {
const id = setInterval((a)= > {
setCount(0 + 1);
}, 1000);
return (a)= >clearInterval(id); },0]); }function Counter() {
useEffect(
(a)= > {
const id = setInterval((a)= > {
setCount(1 + 1);
}, 1000);
return (a)= >clearInterval(id); },1]); }Copy the code
This will solve the problem but our timer will be cleared and reset every time the count changes. This should not be the result we want:
(The dependency has changed, so Effect is rerun.)
** The second strategy is to modify the code inside Effect to ensure that it contains values that change only when needed. ** We don’t want to tell about bad dependencies – we just modify effect to make them less dependent.
Let’s look at some common techniques for removing dependencies.
We want to remove the count dependency on effect.
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= > clearInterval(id);
}, [count]);
Copy the code
To do this, we need to ask ourselves a question: ** Why do we use count? ** You can see that we only use count in the setCount call. In this scenario, we don’t really need to use count in effect. When we want to update the state based on the previous state, we can use the function form of setState:
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(c= > c + 1);
}, 1000);
return (a)= >clearInterval(id); } []);Copy the code
I like to call situations like this “false dependency.” Yes, count is a required dependency because we said setCount(count + 1) in Effect. But what we really want is to convert count to count+1 and then return to React. But React already knows the current count. The only thing we need to tell React is to increment the state – whatever it is now.
That’s exactly what setCount(c => c + 1) does. You can think of it as “sending instructions” to React about how to update the state. This “update form” also helps in other situations, such as when you need to batch update.
Note that we removed dependencies and did not lie. Our effect no longer reads in the rendercount
Value.
(The dependency has not changed, so Effect will not run again.)
You can try it yourself.
Even though Effect only runs once, the timer callback in the first render works perfectly by sending React c => C + 1 updates every time it fires. It no longer needs to know the current count value. Because React already knows.
Functional updates and Google Docs
Remember we said synchronization is the mental model for understanding Effects? One of the interesting things about synchronization is that you usually want to decouple the synchronized “information” from the state. For example, when you edit a document in Google Docs, Google doesn’t send the entire article to the server. That would be very inefficient. Instead, it simply sends your changes to the server in one form.
Although our case for effect is different, a similar idea can be applied. It would be helpful to pass only minimal information in Effects. An updated form like setCount(c => c + 1) passes less information than setCount(count + 1) because it is no longer “tainted” by the current count value. It simply expresses a behavior (” incremental “). Thinking in React also discusses how to find minimum states. The principles are similar, but now the focus is on updating.
Expressing intent (rather than results) is similar to how Google Docs handles co-editing. Although this analogy extends a bit, functional updates play a similar role in React. They ensure that state updates from various sources (event handlers, subscriptions in effects, and so on) can be handled in a batch and predictable manner.
However, even setCount(c => c + 1) is not perfect. It looks a little weird and is very limited in what it can do. For example, if we have two interdependent states, or if we want to calculate the next state based on a prop, it doesn’t do that. Fortunately, setCount(c => c + 1) has a more powerful sister mode, which is called useReducer.
Decouple updates from Actions
Let’s modify the above example to include two states: count and step. Our timer increments count one step at a time:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(c= > c + step);
}, 1000);
return (a)= > clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
Copy the code
(Here’s demo.)
Notice we’re not lying. Since we use step in effect, we add it to the dependency. So that’s why the code works.
The current behavior of this example is that modifying step reboots the timer – because it is one of the dependencies. In most scenarios, this is exactly what you need. There is nothing wrong with clearing the previous effect and re-running the new effect. Unless we have a good reason, we should not change this default behavior.
However, if we don’t want to restart the timer after a step change, how do we remove our dependency on step from Effect?
When you want to update a status and the status update depends on the value of another status, you may need to use ituseReducer
To replace them.
When you write something like setSomething(something =>…) In this case, it may be time to consider using reducer. Reducer allows you to separate what is happening in the component (actions) from how the state is responding and updating.
We replace the Effect step dependency with a dispatch dependency:
const initialState = {
count: 0.step: 1};function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
Copy the code
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect((a)= > {
const id = setInterval((a)= > {
dispatch({ type: 'tick' });
}, 1000);
return (a)= > clearInterval(id);
}, [dispatch]);
Copy the code
(See demo.)
You may ask, “How is this better?” The answer is that React guarantees that dispatch will remain constant for the duration of the component’s declaration cycle. So there is no need to re-subscribe to the timer in the example above.
We solved the problem!
(You can remove dependenciesdispatch
.setState
, anduseRef
The values of the package because React ensures that they are static. But it’s okay if you set them up as dependencies.)
Instead of reading the state directly in effect, it dispatches an _action_ to describe what happened. This 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:
(Here’s the demo if you missed it.)
Why is useReducer a cheat mode for Hooks
We’ve learned how to remove the dependency of effects, whether state updates depend on one state or another. ** But what if we need to rely on props to calculate the next state? ** For example, maybe our API is
In fact, we can avoid it! We can put the _reducer_ function inside the component to read props:
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error(a); } } useEffect((a)= > {
const id = setInterval((a)= > {
dispatch({ type: 'tick' });
}, 1000);
return (a)= > clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
Copy the code
This pattern invalidates some optimizations, so you should avoid abusing it, but you can always access the tools from the Reducer if you need to. (Here’s a demo.)
Even in this example React guarantees that the dispatch is the same on every render. So you can get rid of it in dependencies. It does not cause an effect to repeat itself unnecessarily.
You may be wondering: How is this possible? How does the reducer called in the previous rendering “know” the new props? The answer is that when you dispatch, React simply remembers the action – it will call the Reducer again in the next render. At that point, the new props can be accessed, and the reducer call is not in effect.
That’s why I tend to think, rightuseReducer
It’s “Cheat mode” from Hooks. It can separate the update logic from describing what happened. As a result, this helped me remove unnecessary dependencies and avoid unnecessary effect calls.
Let’s move this function into Effects
A typical misconception is that functions should not be dependencies. For example, the following code seems to work:
function SearchResults() {
const [data, setData] = useState({ hits: []});async function fetchData() {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react',); setData(result.data); } useEffect((a)= >{ fetchData(); } []);Copy the code
(This exampleAdapted from this great article by Robin Wieruch –Click to view!).
To be clear, the above code works. But doing so makes it difficult to ensure that it will work in all situations during iterations of increasingly complex components.
Imagine our code doing the following separation, and each function is five times larger, and we use some state or prop in some function:
function SearchResults() {
const [query, setQuery] = useState('react');
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query; }
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
useEffect((a)= >{ fetchData(); } []); }Copy the code
If we forget to update the dependencies of effects that use these functions (most likely through other function calls), our effects won’t synchronize the changes to props and state. That’s certainly not what we want.
Fortunately, there is a simple solution to this problem. If some functions are called only in effect, you can move their definition to effect:
function SearchResults() { useEffect(() => { function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=react'; } async function fetchData() const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); } []); }Copy the code
(Here’s demo.)
What’s the good of that? We no longer need to think about these “indirect dependencies”. Our dependency arrays don’t lie either: we really don’t use anything in the component scope anymore in our Effect.
If we later modify getFetchUrl to use the Query state, we are more likely to realize that we are editing it in effect – therefore, we need to add query to the effect dependency:
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect((a)= > {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]);
}
Copy the code
(Here’s demo.)
By adding this dependency, we’re not just “pleasing React”. It is reasonable to rerequest data after a Query change. UseEffect is designed to force you to pay attention to changes in the data stream and then decide how our effects should sync with it – rather than ignore it until our users run into bugs.
Thanks to the exhaustive depsLint rule of the eslint-plugin-react-hooks plugin, this will analyze effects as you code and provide suggestions for dependencies that might be missing. In other words, the machine tells you which data flow changes in the component were not handled correctly.
Very good.
But I can’t put this function in Effect
Sometimes you don’t want to move functions into effects. For example, if a component has several effects that use the same function, you don’t want to copy and paste this logic into each effect. Or maybe this function is a prop.
Should you ignore the function dependency in this case? I don’t think so. Again, ** Effects should not lie about its dependence. ** Usually we have a better solution. A common misconception is that “functions never change”. But now that you’ve read this article, you know that’s obviously not true. In fact, the functions defined within the component change with each rendering.
The fact that the function changes every time it renders is a problem in itself. For example, two effects call getFetchUrl:
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect((a)= > {
const url = getFetchUrl('react'); } []); useEffect((a)= > {
const url = getFetchUrl('redux'); } []); }Copy the code
In this case, you probably don’t want to move getFetchUrl into Effects because you want to reuse the logic.
On the other hand, if you are “honest” about dependency, you may fall into a trap. Both of our effects depend on getFetchUrl, which is rendered differently each time, so our dependency array becomes useless:
function SearchResults() {
function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; }
useEffect((a)= > {
const url = getFetchUrl('react');
}, [getFetchUrl]);
useEffect((a)= > {
const url = getFetchUrl('redux');
}, [getFetchUrl]);
}
Copy the code
One possible solution is to remove getFetchUrl from the dependency. But I don’t think this is a good solution. This makes future changes to the data flow harder to detect and forget to deal with. This causes a problem similar to the “timer does not update values” problem above.
Instead, there are two simpler solutions.
First, if a function doesn’t use any values in the component, you should define it outside of the component and then use it freely in Effects:
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect((a)= > {
const url = getFetchUrl('react'); } []); useEffect((a)= > {
const url = getFetchUrl('redux'); } []); }Copy the code
You no longer need to set them as dependencies because they are not in the render scope and therefore will not be affected by the data flow. It can’t suddenly and unexpectedly rely on props or state.
Alternatively, you can wrap it as useCallback Hook:
function SearchResults() {
const getFetchUrl = useCallback((query) = > {
return 'https://hn.algolia.com/api/v1/search?query='+ query; } []); useEffect((a)= > {
const url = getFetchUrl('react');
}, [getFetchUrl]);
useEffect((a)= > {
const url = getFetchUrl('redux');
}, [getFetchUrl]);
}
Copy the code
UseCallback essentially adds a layer of dependency checking. It solves the problem in a different way – we make the function itself change only when needed, rather than removing the dependency on the function.
Let’s see why this works. Previously, our example showed two types of search results (‘react’ and ‘redux’). But if we want to add an input field that allows you to enter any query you want. Instead of passing the Query argument, getFetchUrl now reads from the state.
We quickly discovered that it omitted the Query dependency:
function SearchResults() {
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback((a)= > {
return 'https://hn.algolia.com/api/v1/search?query='+ query; } []); }Copy the code
If I add query to the useCallback dependency, any effect that calls getFetchUrl will be rerun if the query changes:
function SearchResults() {
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback((a)= > {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
useEffect((a)= > {
const url = getFetchUrl();
}, [getFetchUrl]);
}
Copy the code
We have useCallback to thank, because if query remains unchanged, so will getFetchUrl, and our effect will not be rerun. But if query changes, getFetchUrl changes with it, so it rerequests the data. It’s like if you change the value of one cell in Excel, another cell that uses it will automatically recalculate.
This is the result of embracing data flow and synchronous thinking. This method also works for functions passed in from the parent component via attributes:
function Parent() {
const [query, setQuery] = useState('react');
const fetchData = useCallback((a)= > {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
return <Child fetchData={fetchData} />
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]);
}
Copy the code
Because fetchData changes only when the Parent’s Query state changes, our Child will only re-request data when needed.
Is the function part of the data flow?
Interestingly, this pattern does not work in the Class component, and this failure perfectly illustrates the difference between effect and the lifecycle paradigm. Consider the following transformations:
class Parent extends Component {
state = {
query: 'react'
};
fetchData = (a)= > {
const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
};
render() {
return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
render() {
}
}
Copy the code
You’re probably thinking, “Come on Dan, we all know useEffect is like componentDidMount and componentDidUpdate, and you can’t keep breaking that!” Well, adding componentDidUpdate still doesn’t work:
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (this.props.fetchData ! == prevProps.fetchData) {this.props.fetchData();
}
}
render() {
}
}
Copy the code
Of course, fetchData is a class method! (Or you could say the class attribute — but that doesn’t change anything.) FetchData and prevProps. FetchData are always equal, so no request is rerequested. How about we delete the conditional judgment?
componentDidUpdate(prevProps) {
this.props.fetchData();
}
Copy the code
Wait, this will be requested after every render. (Adding a loading animation might be an interesting way to discover this.) Maybe we could bind to a specific Query?
render() {
return <Child fetchData={this.fetchData.bind(this, this.state.query)} / >;
}
Copy the code
But this.props. FetchData! The == prevProps. FetchData expression is always true, even if the query has not changed. This leads us to always ask.
The only realistic way to solve the problem in this class component is to force the Query itself into the Child component. The Child does not actually use the query value directly, but can trigger a re-request when it changes:
class Parent extends Component {
state = {
query: 'react'
};
fetchData = (a)= > {
const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
};
render() {
return <Child fetchData={this.fetchData} query={this.state.query} />; } } class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } componentDidUpdate(prevProps) { if (this.props.query ! == prevProps.query) { this.props.fetchData(); } } render() { } }Copy the code
After using the React class component for so many years, I got so used to passing unnecessary props along and breaking the encapsulation of the parent component that I realized why I had to do it a week ago.
** In a class component, the function properties themselves are not part of the data flow. The ** component’s method contains the mutable this variable, so we cannot assume that it is immutable without doubt. So, even if we only need one function, we have to pass a bunch of data around just to do “diff”. We have no way of knowing whether the passed this.props. FetchData is dependent on the state, and whether its dependent state has changed.
** With useCallback, functions can fully participate in the data flow. ** We can say that if the input of a function changes, the function changes. If not, the function doesn’t change. Thanks to the thoughtful useCallback, changes to properties like props. FetchData are also passed along automatically.
Similarly, useMemo allows us to do similar things with complex objects.
function ColorPicker() {
const [color, setColor] = useState('pink');
const style = useMemo((a)= > ({ color }), [color]);
return <Child style={style} />;
}
Copy the code
** I want to stress that using a useCallback everywhere is pretty clunky. ** useCallback is a great technique and very useful when we need to pass a function along and it will be called in effect on a child component. Or if you’re trying to reduce the burden of memorizing subcomponents, try it. But in general, Hooks themselves do a better job of avoiding passing callback functions.
In the example above, I prefer to put fetchData in my effect (which can be detached into a custom Hook) or bring it in from the top level. I wanted to keep Effects simple, and calling callbacks inside would complicate things. (” What if a props. OnComplete callback changes while the request is still in progress?” ) You can simulate class behavior but that doesn’t solve the race problem.
Tell me about a race
Here is a typical example of making a request in a class component:
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article }); }}Copy the code
As you probably already know, the code above is hiding some problems. It doesn’t handle updates. So the second classic example you can find on the Internet is this:
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if(prevProps.id ! = =this.props.id) {
this.fetchData(this.props.id); }}async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article }); }}Copy the code
That’s obviously better! But there are still problems. The reason for the problem is that the order in which the request results are returned is not guaranteed to be consistent. For example, I request {id: 10} first and then update to {id: 20}, but {id: 20} request comes back first. Requesting earlier but returning later incorrectly overwrites the status value.
This is called a race and is typical in code that mixes async/await (assuming waiting for results to return) with top-down data flow (the props and state may change during an async function call).
Effects does not magically solve this problem, although it will warn you if you pass an async function directly to Effect. (We will improve this warning to better explain these problems you may be experiencing.)
If you use an asynchronous mode that supports cancellation, great. You can cancel asynchronous requests directly in the cleanup function.
Or, the simplest expedient is to track it with a Boolean value:
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect((a)= > {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if(! didCancel) { setArticle(article); } } fetchData();return (a)= > { didCancel = true; };
}, [id]);
}
Copy the code
This article discusses more about how to handle errors and load states, as well as pulling logic away from custom hooks. I recommend you do some reading if you want to learn more about how to request data in Hooks.
Improve the level of
In the class component lifecycle mindset, side effects behave differently than render outputs. UI rendering is powered by props and state and ensures consistency, but side effects are not. This is a common source of problems.
However, in the useEffect thinking model, all are synchronous by default. Side effects become part of the React data flow. For each useEffect call, once you handle it correctly, your component is better able to handle edge cases.
However, the upfront learning cost of using the useEffect well is higher. This can be irritating. Handling edge cases with synchronized code is naturally more difficult than triggering the side effect of not having to step in step with the rendering results once.
This is worrying if useEffect is the tool you use the most right now. At present, however, it is mostly still dealing with the low level of use. Hooks are so new that everyone still uses them at a low level, especially in some of the tutorial examples. But in practice, the community is likely to start using Hooks at a high level, because good apis have better momentum and momentum.
I see different applications creating their own Hooks, such as useFetch, which encapsulates the application authentication logic, or useTheme, which uses theme Context. Once you have a toolkit that includes these, you will not use useEffect directly as often. But every Hook based on it benefits from its ability to adapt.
So far, useEffect has been used mainly for data requests. But data requests are not exactly a synchronization issue. This is especially true because our dependence is often []. So what exactly are we synchronizing?
In the long run, Suspense for data requests will allow third party libraries to tell React to pause rendering in a first class way until something asynchronous (anything: code, data, images) is ready.
As Suspense gradually covers more data request usage scenarios, I expect useEffect to take a back seat as a powerful tool for synchronizing props and state to some side effects. Unlike data requests, it handles these scenarios well because that’s what it’s designed for. But until then, custom Hooks like the ones mentioned here are a good way to reuse data request logic.
In the end of the
Now that you know pretty much everything I know about how to use Effects, check out the TLDR at the beginning. Does it make sense to you now? Did I miss something? (I haven’t finished my paper yet!)
The last
React-hooks UI library: react-hooks UI library
React + Hooks project
Welcome to pay attention to the public number “front-end advanced class” seriously learn the front end, step up together.