useEffect
hook is an extremely powerful an versatile tool, allowing you to even create your own, custom hooks.
useEffect
挂钩是一种功能非常强大的多功能工具,它甚至允许您创建自己的自定义挂钩。
But using it involves surprising amounts of subtlety, so in this article we will see step by step how exactly this hook works.
但是使用它涉及令人惊讶的微妙程度,因此在本文中,我们将逐步了解此挂钩的工作原理。
In order to not lose focus, we will be using the most basic example imaginable and at each step we will control what is happening, by logging messages to the browser console.
为了不失去重点,我们将使用可以想象的最基本的示例,并且在每个步骤中,我们都会通过将消息记录到浏览器控制台来控制正在发生的事情。
You are highly encouraged to follow along this article and code all the examples yourself, using for example an online React repl like this one.
你是高度鼓励沿着这个文章和代码的所有例子自己遵循,例如使用在线阵营REPL像这一个 。
Let’s get started!
让我们开始吧!
基本用途和行为 (Basic use & behavior)
useEffect
is - as the name suggests - a hook to perform arbitrary side effects during a life of a component.
useEffect
是一个在组件生命周期内执行任意副作用的钩子。
It is basically a hook replacement for the “old-school” lifecycle methods componentDidMount
, componentDidUpdate
and componentWillUnmount
.
从根本上讲,它是对“老式”生命周期方法componentDidMount
, componentDidUpdate
和componentWillUnmount
的钩子替换。
It allows you to execute lifecycle tasks without a need for a class component. So you can now make side effects inside a functional component. This
它使您无需类组件即可执行生命周期任务。 因此,您现在可以在功能组件内部产生副作用。 这个
was not possible before, because creating side effects directly in a render
method (or a body of a functional component) is strictly prohibited. Mainly because we don't really control (and shouldn't really think about) how many times render
function will be called.
以前是不可能的,因为严格禁止直接在render
方法(或功能组件的主体)中创建副作用。 主要是因为我们并不真正控制(也不应该真正考虑) render
函数将被调用多少次。
This unpredictability issue is fixed with the use of useEffect
.
使用useEffect
此不可预测性问题。
So let’s create a simple functional component, which we will call Example
:
因此,让我们创建一个简单的功能组件,我们将其称为Example
:
It doesn’t really do anything interesting, because we want to keep it as simple as possible, for the purposes of the explanation.
它实际上并没有做任何有趣的事情,因为出于解释的目的,我们希望使其尽可能简单。
Note that we didn’t use the shortened arrow syntax, where we can simply provide a returned value of a function (in that case a div
element) in place of the body of the function. That's because we already know we will be adding some side effects in that body.
请注意,我们没有使用缩短的箭头语法,在这里我们可以简单地提供函数的返回值(在这种情况下为div
元素)来代替函数的主体。 那是因为我们已经知道我们将在该体内添加一些副作用。
Let’s do just that.
让我们开始吧。
I mentioned previously that it is prohibited to make side effects directly in the body of the component. That’s where the useEffect
hook comes in:
我之前提到过,禁止直接在组件体内产生副作用。 那是useEffect
钩子出现的地方:
As you can see, we used useEffect
function, which accepts a callback function as an argument. Inside the callback we just made a simple console.log
, which will help us find out when this effect is executed.
如您所见,我们使用了useEffect
函数,该函数接受回调函数作为参数。 在回调内部,我们仅创建了一个简单的console.log
,它将帮助我们找出执行此效果的时间。
If you render that component and look into a browser console, you will see render
logged there once.
如果渲染该组件并查看浏览器控制台,则将在此处看到一次render
记录。
Okay. So we know that the callback is for sure called when the component first gets created and rendered. But is that all?
好的。 因此,我们知道在第一次创建和渲染组件时肯定会调用回调。 但这就是全部吗?
In order to find out, we need to make a bit more involved example, that will allow us to rerender the Example
component on command:
为了找出答案,我们需要制作一些更复杂的示例,这将使我们可以在命令中重新呈现Example
组件:
We created a new component called Wrapper
. It renders both our previous component, Example
, and a button. The button displays a counter value, initially set at 0
. After the button is clicked, the counter increases by one.
我们创建了一个名为Wrapper
的新组件。 它同时呈现了我们先前的组件Example
和一个按钮。 该按钮显示一个计数器值,最初设置为0
。 单击按钮后,计数器将增加一。
But the counter itself doesn’t really interests us. we just used it as a trick to cause a rerender of the Example
component. Whenever you click on the counter button, state of Wrapper
component gets updated. This causes a rerender of the Wrapper
, which in turn causes a rerender of the Example
component.
但是柜台本身并不真正使我们感兴趣。 我们只是将其用作导致重新渲染Example
组件的技巧。 只要单击计数器按钮, Wrapper
组件的状态就会更新。 这将导致Wrapper
的重新渲染,而这又将导致Example
组件的重新渲染。
So basically you are causing a rerender of the Example
on each click of the button.
因此,基本上,您每次单击按钮都将导致Example
呈现。
Let’s now click few times on the button and see what is happening in the console.
现在让我们在按钮上单击几次,看看控制台中正在发生什么。
It turns out that after each click, the render
string again appears in the console. So if you click at the button 4 times, you will see 5 render
strings in the console: one from initial render and one from the rerenders that you caused by clicking on the button.
事实证明,每次单击后, render
字符串再次出现在控制台中。 因此,如果您单击按钮4次,您将在控制台中看到5个render
字符串:一个来自初始渲染,一个来自您通过单击按钮导致的重渲染。
Ok, so this means that a callback to useEffect
is called on initial render and every rerender of the component.
好的,这意味着在组件的初始渲染和每次重新渲染时都会调用useEffect
的回调。
Does it get called also when component gets unmounted and disappears from the view? In order to check that, we need to modify the Wrapper
component once more:
当组件卸载并从视图中消失时,它也会被调用吗? 为了进行检查,我们需要再次修改Wrapper
组件:
Now we are rendering Example
conditionally, only if count
is smaller than 5. It means that when the counter hits 5, our component will disappear from the view and React mechanism will trigger it's unmounting phase.
现在,仅在count
小于5时,才有条件地渲染Example
。这意味着当计数器达到5时,我们的组件将从视图中消失,React机制将触发其卸载阶段。
It now turns out that if you click on the counter button 5 times, the render
string will not appear in the console the last time. This means it will appear only once on initial render and 4 times on rerenders on the component, but not on the 5th click, when the component disappears from the view.
现在事实证明,如果您单击计数器按钮5次,则render
字符串将不会最后一次出现在控制台中。 这意味着当组件从视图中消失时,它在组件上的初始渲染中只会出现一次,而在重新渲染时只会出现4次,而在第5次单击时不会出现。
So we learned that unmounting the component does not trigger the callback.
因此,我们了解到卸载组件不会触发回调。
Then how do you create a code that is an equivalent of the componentWillUnmount
lifecycle method? Let's see.
然后,如何创建与componentWillUnmount
生命周期方法等效的代码? 让我们来看看。
If your head spins from all the callbacks, that’s fine — mine does to. But note that we didn’t do anything too crazy. The callback passed to the useEffect
function now returns an another function. You can think of that returned function as a cleanup function.
如果您的头从所有回调中旋转,那很好-我的做到了。 但是请注意,我们没有做任何太疯狂的事情。 传递给useEffect
函数的回调现在返回另一个函数。 您可以将返回的函数视为清理函数。
And here awaits us a surprise. We expected this cleanup function to run only on unmount of the component, that is when counter on our button goes from 4 to 5.
在这里等待着我们一个惊喜。 我们希望此清理功能仅在卸下组件时运行,也就是说,按钮上的计数器从4变为5时。
Yet that is not what happens. If you run this example in the console, you will see that string unmount
appears in the console at the end when component is unmounted, but also when the component is about to be rerendered.
然而,事实并非如此。 如果您在控制台中运行这个例子,你会看到字符串unmount
出现在控制台末时组件卸载, 而且当组件将要被重新描绘。
So in the end, the console looks like that:
所以最后,控制台看起来像这样:
You can see that every render
(when the useEffect
main callback gets executed) is accompanied by respective unmount
(when the cleanup function is executed).
您可以看到每个render
(执行useEffect
主回调时)都useEffect
相应的unmount
(执行清除功能时)。
Those two “phases” — effect and cleanup — always go in pairs.
这两个“阶段”(效果和清除)始终成对出现。
So we see that this model differs from traditional lifecycle callbacks of a class components. It seems to be a bit stricter and more opinionated.
因此,我们看到此模型不同于类组件的传统生命周期回调。 它似乎更严格,更自以为是。
But why was it designed this way? In order to find out, we need to learn how useEffect
hook cooperates with component props.
但是为什么要这样设计呢? 为了找出useEffect
,我们需要学习useEffect
挂钩如何与组件props配合使用。
使用效果和道具 (useEffect & props)
Our Wrapper
component already has a state - count
- that we can pass into Example
component, to see how its useEffect
will behave with the props.
我们的Wrapper
组件已经有一个状态- count
-我们可以传递到Example
组件,看其如何useEffect
将与道具的行为。
We modify Wrapper
component in the following way:
我们通过以下方式修改Wrapper
组件:
And then we update the Example
component itself:
然后我们更新Example
组件本身:
It turns out that simply passing the counter as a prop or even displaying it in div
element of the component does not change the behavior of the hook in any way.
事实证明,仅将计数器作为道具传递,甚至将其显示在组件的div
元素中都不会以任何方式改变挂钩的行为。
What is more, using this prop in useEffect
behaves as we would expect, while also giving us a bit more insight into how useEffect
s main callback and cleanup functions are related.
而且,在useEffect
使用此道具的行为符合我们的预期,同时也使我们对useEffect
的主要回调和清除函数之间的关系useEffect
更深入的了解。
This code, where we simply add count
prop to our logs:
此代码,我们在其中简单地将count
属性添加到日志中:
will result in the following output, when you start clicking on the counter button:
当您开始点击计数器按钮时,将产生以下输出:
This might seem like a trivial result, but it enforces what we learned about the main callback of useEffect
and its cleanup function - they always go in paris.
这看起来似乎是微不足道的结果,但是它可以强制执行我们所了解的useEffect
的主回调及其清理功能-它们始终位于巴黎。
Note that each cleanup function even utilizes the same props as its respective callback.
请注意,每个清理函数甚至使用与其各自的回调相同的道具。
For example first callback has count set to 0 and its cleanup function utilizes the same value, instead of 1, which belongs to the next pair of the effect and cleanup.
例如,第一个回调的count设置为0,并且其清除函数使用相同的值,而不是1,该值属于效果和清除的下一个对。
This is a key to the design of the useEffect
hook. Why is that so important, you might ask?
这是useEffect
挂钩设计的关键。 您可能会问,为什么这么重要?
Imagine for example that your component has to establish a connection to a service with a following API:
例如,假设您的组件必须使用以下API建立与服务的连接:
This service requires you to unsubscribe with exactly the same id
that you used to subscribe to it in the first place. If you don't do that, you will leave an opn connection, which will cause leaks that ultimately might even crash the service!
此服务要求您退订时使用与最初订阅时使用的id
完全相同的id
。 如果不这样做,将留下一个opn连接,这将导致泄漏,最终甚至可能导致服务崩溃!
Luckily useEffect
enforces a proper design with its architecture.
幸运的是, useEffect
对其架构实施了适当的设计。
Note that if id
required by the Service
is passed via props to the component, all you have to do is to write inside that component:
请注意,如果Service
需要的id
通过props传递给组件,则您要做的就是在该组件内部编写:
As we have seen with our logging examples, useEffect
will make sure that each subscribe
is always followed by unsubscribe
, with exactly the same id
value passed to it.
正如我们在日志记录示例中看到的那样, useEffect
将确保每个subscribe
始终跟随着unsubscribe
,并为其传递完全相同的id
值。
This architecture makes writing sound and safe code very straightforward, no matter how often the component updates and no matter how frantically its props are changing.
这种结构使编写声音和安全代码变得非常简单,无论组件更新的频率如何,以及其道具的更改频率如何。
控制更新 (Controlling the updates)
For people who got used to class component lifecycle methods, useEffect
often seems limiting at the beginning.
对于习惯于对组件生命周期方法进行分类的人们, useEffect
在开始时往往似乎很局限。
How do you add an effect only at the very first render?
如何只在最初的渲染中添加效果?
How do you run a cleanup function only at the end of components life, instead of after every rerender?
您如何只在组件寿命结束时而不是每次重新渲染后运行清除功能?
In order to find out the answers to those questions, we need to describe one last mechanism that useEffect
offers to us.
为了找到这些问题的答案,我们需要描述useEffect
提供给我们的最后一种机制。
As a second argument, useEffect
optionally accepts an array of values. Those values will be then compared to the previous values, when deciding if the effect should be ran or not.
作为第二个参数, useEffect
可以选择接受一个值数组。 然后,在决定是否应运行效果时,将这些值与以前的值进行比较。
It works a bit like shouldComponentUpdate
for side effects. If the values changed, the effects will be ran. If none of the values changed, nothing will happen.
它的工作原理与shouldComponentUpdate
,可带来副作用。 如果值更改,则将运行效果。 如果这些值均未更改,则不会发生任何事情。
So we can edit our Example
component like so:
因此,我们可以像这样编辑Example
组件:
Because our useEffect
function used count
prop and because we want to log a string to the console every time the count changes, we provided a second argument to the useEffect
- an array with only one value, namely the prop which we want to observe for changes.
因为我们的useEffect
函数使用了count
道具,并且因为我们希望每次计数变化时都将字符串记录到控制台,所以我们为useEffect
提供了第二个参数-一个只有一个值的数组,即我们要观察其变化的道具。
If between rerenders the value of count
does not change, the effect will not be ran and no log with appear in the console.
如果在两次渲染之间不改变count
的值,则效果将不会运行,并且控制台中不会显示任何日志。
In order to see that it’s really what happens, we can edit our Wrapper
component:
为了看到它确实发生了,我们可以编辑Wrapper
组件:
You can see that we are now rendering two Example
components. One - as before - gets passed count
value as a prop, while the other gets always the same value of -1.
您可以看到我们现在正在渲染两个Example
组件。 一个-和以前一样-获得传递的count
数值作为prop,而另一个始终获得相同的值-1。
This will allow us to compare the difference in the console outputs, when we click repeatedly on the counter button. Just remember to include [count]
array as a second parameter to useEffect
.
当我们反复单击计数器按钮时,这将使我们能够比较控制台输出的差异。 只要记住包括[count]
数组作为useEffect
的第二个参数即可。
After clicking on the counter several times, we get:
多次点击计数器后,我们得到:
So, as you can see, if you include count
in the array of the second argument to useEffect
, the hook will only be triggered when the value of the prop changes and at the beginning and the end of the life of the component.
因此,如您所见,如果将count
包含在useEffect
的第二个参数的数组中,则仅当prop的值更改并且在组件寿命的开始和结束时才触发该挂钩。
So, because our second Example
component had -1 passed as count
the entire time, we only saw two logs from it - when it was first mounted and when it was dismounted (after count < 5
condition began to be false).
因此,由于我们的第二个Example
组件在整个时间内都传递了-1的count
,因此我们仅从中看到两个日志-首次安装和卸下时( count < 5
条件为假)。
Even if we would provide some other props to the Example
component and those props would be changing often, the second component would still log only twice, because it now only watches for changes in count
prop.
即使我们为Example
组件提供了一些其他支持,并且这些支持经常变化,第二个组件仍然只会记录两次,因为它现在仅监视count
支持的变化。
If you wanted to react to changes of some other props, you would have to include them in the useEffect
array.
如果您想对其他一些道具的变化做出React,则必须将其包含在useEffect
数组中。
On the other hand, in the first Example
component from the snippet, value of the count
prop was increasing by one on every click on the button, so this component was making logs every time.
另一方面,在摘录中的第一个Example
组件中,每次单击按钮时count
道具的值增加一个,因此该组件每次都在进行日志记录。
Let’s now answer a questions that we asked ourselves earlier. How do you make a side effect that runs only at the beginning and at the end of components lifecycle?
现在让我们回答一个我们之前问自己的问题。 您如何产生仅在组件生命周期的开始和结束时产生的副作用?
It turns out that you can pass even an empty array to the useEffect
function:
事实证明,您甚至可以将一个空数组传递给useEffect
函数:
Because useEffect
only triggers callbacks at the mount and unmount, as well as value changes in the array, and there is no values in the array, the effects will be called only at the beginning and the end of components life.
因为useEffect
仅在装入和卸载以及数组中的值更改时触发回调,并且数组中没有值,所以仅在组件寿命的开始和结束时才调用效果。
So now in the console you will see render
when the component gets rendered for the first time and unmount
when it disappears. Rerenders will be completely silent.
因此,现在在控制台中,您将在首次获取组件时看到render
,并在消失时unmount
。 渲染器将完全保持沉默。
摘要 (Summary)
That was probably a lot of to digest. So let’s make a brief summary, that will help you remember the most important concepts from this article:
这可能要消化很多。 因此,让我们做一个简短的总结,它将帮助您记住本文中最重要的概念:
useEffect
hook is a mechanism for making side effects in functional components. Side effects should not be caused directly in components body orrender
function, but should always be wrapped in a callback passed touseEffect
.useEffect
挂钩是一种在功能组件中产生副作用的机制。 副作用不应直接在组件主体或render
函数中引起,而应始终包装在传递给useEffect
的回调中。- You can optionally return in the callback another callback, which should be used for cleanup purposes. The main callback and cleanup callback are always triggered in pairs, with exactly the same props. 您可以选择在回调中返回另一个回调,该回调应用于清理目的。 主回调和清理回调总是成对触发,并且道具完全相同。
By default
useEffect
callback (and corresponding cleanup) is ran on initial render and every rerender as well as on dismount. If you want to change that behaviour, add an array of values as a second argument to theuseEffect
. Then the effects will be ran only on mount and unmount of the component or if the values in that array changed. If you want to trigger the effects only on mount and unmount, simply pass an empty array.默认情况下,
useEffect
回调(和相应的清理)在初始渲染,每次重新渲染以及卸除时运行。 如果要更改该行为,请向useEffect
添加值数组作为第二个参数。 这样,效果将仅在组件的安装和卸载或该阵列中的值更改时运行。 如果要触发的影响只在装载和卸载,只需通过一个空数组。
So that’s it! I hope this article helped you deeply understand how useEffect
works.
就是这样了! 希望本文能帮助您深入了解useEffect
工作原理。
It might seem like a basic and easy hook, but now you see just how much complexity and subtlety is behind it.
这看起来像是一个简单的基本钩子,但是现在您看到了它背后的复杂性和微妙之处。
If you enjoyed this article, considered following me on Twitter, where I will be posting more articles on JavaScript programming.
如果您喜欢这篇文章,请考虑在Twitter上关注我,我将在其中发布更多有关JavaScript编程的文章。
Thanks for reading!
谢谢阅读!
(Cover Photo by milan degraeve on Unsplash)
(封面照片由米兰在《 Unsplash》上摄制 )
Originally published at https://dev.to on August 8, 2020.
最初于 2020年8月8日 发布在 https://dev.to 。