The useEffect cleanup and the two circumstances it's called.
useEffect 清理和它被调用的两种情况。
反应钩子效果中级
See Our Public Workshops:
查看我们的公开研讨会:
The cleanup function is a function returned from within the effect function. It gets called when the component unmounts but you probably already knew that.
清理函数是从效果函数内部返回的一个函数。当组件卸载时它会被调用,不过你可能已经知道这一点。
In my workshops I ask developers when the function gets called and I regularly get this one answer. But in the 100s of workshops I've taught over the years, I think I've only ever heard the full correct answer from one person (hats off to that person).
在我的讲座中,我会问开发者函数何时被调用,我经常得到一个答案。但在我多年来教授的数百场讲座中,我觉得我只听过一个人给出完整的正确答案(向那个人致敬)。
useEffect(() => {getUser(userId).then((user) => {setUser(user)})// Cleanup Function: Called when we unmountreturn () => {}}, [userId])
You're probably skimming this article and want to jump strait to the second circumstance it gets called:
您可能在浏览这篇文章,并想直接跳到被称为第二种情况的部分:
The cleanup also gets called when the dependency array changes and the effect needs to run again. But it's the previous effect's cleanup that runs before the next effect function runs. You might need the full article to really understand...
当依赖数组发生变化并且效果需要再次运行时,清理函数也会被调用。但在下一个效果函数运行之前,会先运行前一个效果的清理函数。您可能需要阅读全文才能真正理解……
Why Cleanup 为什么要清理
To better understand both of the circumstances it's called, we need to do some examples of why you would want to do a cleanup in the first place.
为了更好地理解这两种情况,我们需要举一些例子说明为什么您首先想要进行清理。
In the code above, the most well-known reason is unfortunately the most misguided one so we'll start there. People think we need to cleanup because if we don't, we might "set state on an unmounted component".
在上面的代码中,最著名的原因不幸地是最误导的原因,因此我们将从那里开始。人们认为我们需要清理,因为如果不清理,我们可能会“在未挂载的组件上设置状态”。
We have another post where we do a deep dive into why you shouldn't care about fixing this particular problem, but it is a great starting point because we need this same fix for other reasons we'll show later.
我们有另一篇文章,深入探讨了为什么你不必担心解决这个特定问题,但这是一个很好的起点,因为我们出于其他原因需要相同的解决方案,我们将在后面展示。
Preventing our component from setting state when it's unmounted can look like this:
在组件卸载时防止其设置状态可以如下所示:
useEffect(() => {// 1. After the component renders, the useEffect function is called// and we're guaranteed to be mounted at this point so set this flaglet isMounted = truegetUser(userId).then((user) => {if (mounted) {setUser(user)}})// 2. We do the actual side effect (getUser) and now we need to return// a way of "cleaning up" any problems that might occur because of it.return () => {isMounted = false}}, [userId])
The major takeaway so far is that we're returning the cleanup but not calling it, and when we return this cleanup, the promise is still pending.
目前主要的收获是,我们正在返回清理工作但没有调用它,并且当我们返回这个清理工作时,承诺仍然处于待定状态。
Now, the race condition begins. Is the component going to unmount first (maybe we navigated to another page) before the promise resolves? Or is the promise going to resolve first?
现在,竞态条件开始了。组件会在 promise 解析之前先卸载吗(可能我们已经导航到另一个页面)?还是 promise 会先解析?
If the component unmounts first before the promise resolves, this code will prevent us from setting state on an unmounted component -- if that's what you wanted to do. I'm saying that doesn't matter
如果组件在 Promise 解析之前先卸载,则此代码将阻止我们在未卸载组件上设置状态——如果这正是您想要做的。我说这无所谓
To be clear, I do want the cleanup solution we wrote but only because it fixes a different problem, a race condition.
为了明确,我确实想要我们编写的清理解决方案,但这仅仅是因为它修复了一个不同的问题,一个竞争条件。
Race conditions 竞争条件
Let's go back to before we had our isMounted
code:
让我们回到我们有 isMounted
代码之前的时刻:
function UserProfile({ userId }) {const [user, setUser] = useState(null)useEffect(() => {getUser(userId).then((user) => {setUser(user)})return () => {}}, [userId])return <div>...</div>}
In this UserProfile
component, let's imagine we can click on friends of this user. Perhaps we're looking at users/1
now but we could quickly click and go to users/2
, then users/3
, then users/4
, and finally users/5
. All these clicks would cause our component to re-render with a new userId
prop.
在这个 UserProfile
组件中,假设我们可以点击该用户的朋友。也许我们现在正在查看 users/1
,但我们可以快速点击并转到 users/2
,然后 users/3
,再然后 users/4
,最后 users/5
。所有这些点击都会导致我们的组件使用新的 userId
属性重新渲染。
We make these several clicks very fast and ultimately users/5
should be the one we end up seeing since it was the last one clicked. Here's what happens though...
我们快速进行几次点击,最终 users/5
应该是我们最终看到的,因为这是最后一次点击的。可是,事情是这样的……
Each time we click, the re-render and new userId
means we re-run the effect function based on that new userId
. We now have a race-condition where the network requests could return in a different order than we sent them. The network request that resolves last wins. We want to be looking at users/5
but perhaps the network request for users/4
was a lot slower than the rest and it resolves last. You're now incorrectly looking at User 4.
每次我们点击,重新渲染和新的 userId
意味着我们基于新的 userId
重新运行效果函数。现在我们有一个竞争条件,网络请求可能以与发送顺序不同的顺序返回。最后解析的网络请求胜出。我们希望查看 users/5
,但也许 users/4
的网络请求比其他请求慢得多,并且最后解析。你现在错误地查看了用户 4。
The other circumstance the cleanup gets called...
另一种情况是清理工作被叫来...
The cleanup function will get called when we "switch" effects. In other words when the dependency array changes and we're about to run the useEffect()
function again. React will run the previous effect's cleanup just before we run a new effect. We can illustrate this with a timeline.
清理函数将在我们“切换”效果时被调用。换句话说,当依赖数组发生变化并且我们即将再次运行useEffect()
函数时,React 会在运行新效果之前运行上一个效果的清理。我们可以用时间线来说明这一点。
Timeline... 时间线...
Conceptually, this is what it's like when React calls our function component, let's think of the effect that runs as being the "current effect":
从概念上讲,这就是当 React 调用我们的函数组件时的情况,我们可以将运行的副作用视为“当前副作用”
UserProfile() // useEffect runs based on user 1: this is the current effect
Lets say we get some re-renders that have nothing to do with the userId
changing. React calls our component again but the current effect still belongs to that first render when it ran:
假设我们有一些与userId
的变化无关的重新渲染。React 再次调用我们的组件,但当前的效果仍然属于第一次渲染时运行的效果:
UserProfile() // <-- current effectUserProfile() // re-renderUserProfile() // re-render
So you can see that we might have a "recent render", but the "current effect" was from a previous render.
所以你可以看到我们可能有一个“最近的渲染”,但“当前的效果”是来自之前的渲染。
Then the userId
changes and we get a re-render again. This re-render needs to run the useEffect()
again. But wait, before we do that we need to "cleanup" the old "current effect" first.
然后 userId
发生变化,我们又会重新渲染一次。这个重新渲染需要再次运行 useEffect()
。但等一下,在我们这样做之前,我们需要先“清理”旧的“当前效果”。
UserProfile() // 1. cleanup this effect firstUserProfile()UserProfile()UserProfile() // 2. run this effect after the previous cleanup runs (now current)
If we do a cleanup like this, we can fix our race condition:
如果我们进行这样的清理,我们就可以修复我们的竞争条件:
useEffect(() => {let isCurrent = truegetUser(userId).then((user) => {if (isCurrent) {setUser(user)}})return () => {isCurrent = false}}, [userId])
Hey look, it's the exact same solution as the one we used to not set state on an unmounted component only the variable is more appropriately named because we now understand that the function isn't only called when we unmount.
嘿,看,这个解决方案和我们用来不在未安装组件上设置状态的完全一样,只不过变量的命名更加合理,因为我们现在明白这个函数不仅在我们卸载时被调用。
With this cleanup, every time the userId
changes, we cleanup the previous effect first which prevents it from setting state when it resolves. Then we run the effect for the next user we want to see and it now becomes the current effect.
通过这次清理,每当 userId
发生变化时,我们首先清理 之前 的效果,这样可以防止 它 在解析时设置状态。 然后 我们运行下一个用户的效果,并且它现在成为当前效果。
When this happens really fast and we change the current effect while we still have pending promises, the net result is we avoid the race condition by "canceling" the previous effects ability to set state. We want to only see users/5
when it resolves and all previous effects will not be able to set state regardless of when they resolve.
当这种情况发生得非常迅速时,而我们在仍然有未决 Promise 的情况下改变当前效果,最终结果是我们通过“取消”先前效果设置状态的能力来避免竞争条件。我们希望仅在解析时看到 users/5
,并且所有之前的效果都无法设置状态,无论它们何时解析。
It's interesting though that this solution also prevents us from setting state on an unmounted component -- not that we care about that, but it's interesting.
有趣的是,这个解决方案还防止我们在未挂载的组件上设置状态——并不是我们在乎这个,但这确实有趣。
In a way, you could conceptualize these two circumstances as being combined into one rule:
在某种程度上,你可以将这两种情况概念化为一个规则
We cleanup when an effect is no longer relevant. It's not relevant when we unmount and when we need to ditch an old effect to start a new one. However you think of it, as long as you understand it I'm okay with that.
当一个效果不再相关时,我们就会清理。当我们卸载时,它不再相关,当我们需要放弃一个旧效果以启动一个新效果时也是如此。无论你怎么想,只要你能理解,我就没问题。
Happy coding. 快乐编程。
Instead of having comments here in our blog, we've tweeted about this post so you can comment there if you wish.
为了避免在我们的博客中留言,我们在推特上发布了关于这篇文章的信息,如果您愿意,可以在那里评论。
由 Todd Steitle 在 Unsplash 上拍摄的照片