使用 useEffect,如何跳过在初始渲染时应用效果?

Posted

技术标签:

【中文标题】使用 useEffect,如何跳过在初始渲染时应用效果?【英文标题】:With useEffect, how can I skip applying an effect upon the initial render? 【发布时间】:2019-04-10 06:28:10 【问题描述】:

使用 React 的新效果挂钩,如果某些值在重新渲染之间没有改变,我可以告诉 React 跳过应用效果 - React 文档中的示例:

useEffect(() => 
  document.title = `You clicked $count times`;
, [count]); // Only re-run the effect if count changes

但上面的示例将效果应用于初始渲染,以及count 已更改的后续重新渲染。如何告诉 React 跳过初始渲染的效果?

【问题讨论】:

你看过React.useMemo吗? 【参考方案1】:

如指南所述,

Effect Hook,useEffect,增加了从函数组件执行副作用的能力。它的用途与 React 类中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 相同,但统一为一个 API。

在本指南的示例中,预计 count 仅在初始渲染时为 0:

const [count, setCount] = useState(0);

所以它将作为componentDidUpdate 工作并进行额外检查:

useEffect(() => 
  if (count)
    document.title = `You clicked $count times`;
, [count]);

这基本上是可以用来代替useEffect 的自定义钩子的工作方式:

function useDidUpdateEffect(fn, inputs) 
  const didMountRef = useRef(false);

  useEffect(() => 
    if (didMountRef.current)  
      return fn();
    
    didMountRef.current = true;
  , inputs);

感谢 @Tholle 建议 useRef 而不是 setState

【讨论】:

useRef 优于 setState 的建议在哪里?我在此页面上没有看到它,我想了解原因。 @rob-gordon 这是更新答案后删除的评论。原因是 useState 会导致不必要的组件更新。 这种方法有效,但它违反了 react-hooks/exhaustive-deps linter 规则。 IE。 fn 未在 deps 数组中给出。有人有不违反 React 最佳实践的方法吗? @JustinLang React linter rules != 最佳实践,他们只尝试解决常见的钩子问题。 ESLint 规则并不智能,可能会导致误报或误报。只要钩子背后的逻辑是正确的,就可以使用 eslint-disable 或 eslint-disable-next 注释安全地忽略规则。在这种情况下,fn 不应作为输入提供。请参阅说明,reactjs.org/docs/…。如果fn 内部的某些东西引入了deps,则更像是应该直接将它们提供为inputs 注意:如果您使用多个 useEffects 检查 didMountRef,请确保只有最后一个(底部)将 didMountRef 设置为 false。 React 按顺序遍历 useEffects!【参考方案2】:

这是一个自定义钩子,它只提供一个布尔标志来指示当前渲染是否是第一个渲染(当组件被安装时)。它与其他一些答案大致相同,但您可以在 useEffect 或渲染函数或您想要的组件中的任何其他位置使用标志。也许有人可以提出一个更好的名字。

import  useRef, useEffect  from 'react';

export const useIsMount = () => 
  const isMountRef = useRef(true);
  useEffect(() => 
    isMountRef.current = false;
  , []);
  return isMountRef.current;
;

你可以像这样使用它:

import React,  useEffect  from 'react';

import  useIsMount  from './useIsMount';

const MyComponent = () => 
  const isMount = useIsMount();

  useEffect(() => 
    if (isMount) 
      console.log('First Render');
     else 
      console.log('Subsequent Render');
    
  );

  return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
;

如果你有兴趣,这里有一个测试:

import  renderHook  from '@testing-library/react-hooks';

import  useIsMount  from '../useIsMount';

describe('useIsMount', () => 
  it('should be true on first render and false after', () => 
    const  result, rerender  = renderHook(() => useIsMount());
    expect(result.current).toEqual(true);
    rerender();
    expect(result.current).toEqual(false);
    rerender();
    expect(result.current).toEqual(false);
  );
);

我们的用例是隐藏动画元素,如果初始道具表明它们应该被隐藏。如果道具发生变化,在以后的渲染中,我们确实希望元素动画出来。

【讨论】:

导入从react-hooks-testing-library更改为@testing-library/react-hooksgithub.com/testing-library/react-hooks-testing-library/releases/… 为什么选择“isMount”而不是“didMount”或“isMounted”? 猜猜我在考虑当前渲染是否是发生安装的渲染。是的,我同意,现在回到它听起来有点奇怪。但它在第一次渲染时是真的,之后是假的,所以你的建议听起来会产生误导。 isFirstRender 可以工作。 感谢钩子!我同意@ScottyWaggoner,isFirstRender 是一个更好的名字 添加测试是 FIRE!反响很好。【参考方案3】:

我找到了一个更简单的解决方案,不需要使用另一个钩子,但是它有缺点。

useEffect(() => 
  // skip initial render
  return () => 
    // do something with dependency
  
, [dependency])

这只是一个例子,如果您的情况很简单,还有其他方法可以做到这一点。

这样做的缺点是不能产生清理效果,只会在依赖数组第二次发生变化时执行。

不建议使用,您应该使用其他答案所说的内容,但我只是在这里添加了这个,所以人们知道这样做的方法不止一种。

编辑:

为了更清楚,您不应该使用这种方法来解决问题中的问题(跳过初始渲染),这仅用于教学目的,表明您可以做到同一件事以不同的方式。 如果您需要跳过初始渲染,请使用其他答案的方法。

【讨论】:

我刚刚学到了一些东西。我不认为这会起作用,然后我尝试了它,它确实起作用了。谢谢! React 应该为此提供更好的方法,但是这个问题在 Github 上是开放的,并且建议自己编写一个自定义解决方案(这完全是无意义的)。 那个的问题是它也会在unMount上执行,并不完美。 @ncesar 如果这是唯一有效的答案,可能你做错了什么,因为另一个答案是正确的,而我的只是为了教学目的【参考方案4】:

我使用常规状态变量而不是 ref。

// Initializing didMount as false
const [didMount, setDidMount] = useState(false)

// Setting didMount to true upon mounting
useEffect(() =>  setDidMount(true) , [])

// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() => 
  if (didMount) document.title = `You clicked $count times`
, [count])

我们可以像这样将 didMount 逻辑重构为自定义钩子。

function useDidMount() 
  const [didMount, setDidMount] = useState(false)
  useEffect(() =>  setDidMount(true) , [])

  return didMount

最后,我们可以像这样在我们的组件中使用它。

const didMount = useDidMount()

const [count, setCount] = useState(0)
useEffect(() => 
  if (didMount) document.title = `You clicked $count times`
, [count])

更新使用 useRef 挂钩来避免额外的重新渲染(感谢@TomEsterez 的建议)

这次我们的自定义钩子返回一个函数,返回我们的 ref 的当前值。你也可以直接使用 ref,但我更喜欢这个。

function useDidMount() 
  const mountRef = useRef(false);

  useEffect(() =>  mountRef.current = true , []);

  return () => mountRef.current;

用法

const MyComponent = () => 
  const didMount = useDidMount();

  useEffect(() => 
    if (didMount()) // do something
    else // do something else
  )

  return (
    <div>something</div>
  );

顺便说一句,我从来没有使用过这个钩子,而且可能有更好的方法来处理这个问题,这将更符合 React 编程模型。

【讨论】:

useRef 更适合这种情况,因为useState 会导致组件的额外且无用的渲染:codesandbox.io/embed/youthful-goldberg-pz3cx 您的useDidMount 函数中有错误。您必须用大括号 ...mountRef.current = true 封装在 useEffect() 中。离开他们就像写return mountRef.current = true,这会导致像An effect function must not return anything besides a function, which is used for clean-up. You returned: true这样的错误【参考方案5】:

一个 TypeScript 和 CRA 友好的钩子,将其替换为 useEffect,这个钩子的工作方式类似于 useEffect,但不会在第一次渲染发生时触发。

import * as React from 'react'

export const useLazyEffect:typeof React.useEffect = (cb, dep) => 
  const initializeRef = React.useRef<boolean>(false)

  React.useEffect((...args) => 
    if (initializeRef.current) 
      cb(...args)
     else 
      initializeRef.current = true
    
  // eslint-disable-next-line react-hooks/exhaustive-deps
  , dep)

【讨论】:

【参考方案6】:

这是我基于 Estus Flask 的 answer 用 Typescript 编写的实现。它还支持清理回调。

import  DependencyList, EffectCallback, useEffect, useRef  from 'react';

export function useDidUpdateEffect(
  effect: EffectCallback,
  deps?: DependencyList
) 
  // a flag to check if the component did mount (first render's passed)
  // it's unrelated to the rendering process so we don't useState here
  const didMountRef = useRef(false);

  // effect callback runs when the dependency array changes, it also runs
  // after the component mounted for the first time.
  useEffect(() => 
    // if so, mark the component as mounted and skip the first effect call
    if (!didMountRef.current) 
      didMountRef.current = true;
     else 
      // subsequent useEffect callback invocations will execute the effect as normal
      return effect();
    
  , deps);

现场演示

下面的现场演示演示了 useEffectuseDidUpdateEffect 钩子之间的区别

【讨论】:

【参考方案7】:

让我给你介绍react-use。

想跑:

仅在第一次渲染后useUpdateEffect

只有一次useEffectOnce

是第一次挂载吗? useFirstMountState

想要通过深度比较浅比较节流运行效果?更多here。

不想安装库?检查code 并复制。 (也可以为那里的好人提供star

【讨论】:

【参考方案8】:

以下解决方案与上面类似,只是我更喜欢更清洁的方式。

const [isMount, setIsMount] = useState(true);
useEffect(()=>
        if(isMount)
            setIsMount(false);
            return;
        
        
        //Do anything here for 2nd render onwards
, [args])

【讨论】:

这不是一个理想的方法,因为设置状态会导致另一个渲染【参考方案9】:

我打算对当前接受的答案发表评论,但空间不足!

首先,在使用功能组件时,不要考虑生命周期事件,这一点很重要。考虑道具/状态的变化。我遇到过类似的情况,我只希望在特定道具(在我的情况下为 parentValue)从其初始状态更改时触发特定的 useEffect 函数。因此,我创建了一个基于其初始值的 ref:

const parentValueRef = useRef(parentValue);

然后在useEffect fn 的开头包含以下内容:

if (parentValue === parentValueRef.current) return;
parentValueRef.current = parentValue;

(基本上parentValue没有变化就不要运行效果了,如果有变化就更新ref,准备下一次检查,继续运行效果)

因此,尽管建议的其他解决方案可以解决您提供的特定用例,但从长远来看,它将有助于改变您对功能组件的看法。

将它们视为主要基于某些道具渲染组件。

如果您确实需要一些本地状态,那么 useState 会提供,但不要假设您的问题将通过存储本地状态来解决。

如果你有一些代码会在渲染过程中改变你的道具,这个“副作用”需要被包裹在 useEffect 中,但这样做的目的是获得一个不受渲染时发生变化。 useEffect 钩子将在渲染完成后运行,并且正如您所指出的,它在每次渲染时运行 - 除非第二个参数用于提供道具/状态列表以识别哪些更改的项目将导致它将在以后运行。

祝您在功能组件/钩子之旅上好运!有时有必要忘记一些东西来掌握一种新的做事方式:) 这是一本优秀的入门书:https://overreacted.io/a-complete-guide-to-useeffect/

【讨论】:

以上是关于使用 useEffect,如何跳过在初始渲染时应用效果?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 react 中的新路由更改上使用 useEffect 重新渲染变量

如何在 React Native 应用程序中使用 React hook useEffect 为每 5 秒渲染设置间隔?

如何避免在每次渲染时触发useEffect?

初始渲染时未定义 useRef 值

为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?

如何跳过在测试代码上运行 scalafmt?