一个简单的例子深入解释了React.useEffect钩子

本文深入讲解React中的useEffect Hook,揭示其工作原理及如何在组件生命周期内产生副作用。通过实例演示,展示useEffect与组件props的交互作用,以及如何通过控制更新来优化组件性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.

从根本上讲,它是对“老式”生命周期方法componentDidMountcomponentDidUpdatecomponentWillUnmount的钩子替换。

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 or render function, but should always be wrapped in a callback passed to useEffect.

    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 the useEffect. 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

翻译自: https://medium.com/@mpodlasin/react-useeffect-hook-explained-in-depth-on-a-simple-example-ec9f898d32d3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值