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

213

使用React新的Effect Hooks,我可以告诉React在重新渲染时跳过应用效果,如果特定的值没有发生变化-来自React文档的示例:

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

但是上面的例子应用了效果在初始渲染和count更改之后的重新渲染上。我该如何告诉React跳过初始渲染时的效果呢?


2
你有研究过 React.useMemo 吗? - Pankaj Parag
11个回答

224
根据指南所述,
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 isMountingRef = useRef(false);

  useEffect(() => {
    isMountingRef.current = true;
  }, []);

  useEffect(() => {
    if (!isMountingRef.current) {
      return fn();
    } else {
      isMountingRef.current = false;
    }
  }, inputs);
}

8
useRef 相对于 setState 的建议在哪里?我在这个页面上没有看到,我想了解为什么。 - rob-gordon
11
这是一个在答案更新后被删除的评论。它的理由是useState会导致不必要的组件更新。 - Estus Flask
2
这种方法是可行的,但它违反了react-hooks/exhaustive-deps的检查规则。即fn没有在依赖数组中给出。有没有一种不违反React最佳实践的方法? - Justin Lang
14
@JustinLang React的代码检查规则并不等同于最佳实践,它们只是尝试解决常见的钩子问题。ESLint规则并不智能,可能会导致误报或漏报。只要钩子背后的逻辑是正确的,就可以安全地使用eslint-disableeslint-disable-next注释忽略某个规则。在这种情况下,fn不应该作为输入提供。请参阅说明 https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies 。如果fn中的某些内容引入了依赖项,则更好的做法是直接将它们作为inputs提供。 - Estus Flask
4
注意:如果您正在使用多个检查didMountRef的useEffect,请确保只有最后一个(最底部的一个)将didMountRef设置为false。React按顺序遍历useEffect! - Dror Bar
显示剩余11条评论

115
这是一个自定义钩子,仅提供一个布尔标志,用于指示当前渲染是否为首次渲染(当组件被挂载时)。它与其他一些答案差不多,但您可以在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);
  });
});

我们的使用情况是,如果初始属性表明它们应该被隐藏,就隐藏动画元素。在后续渲染中,如果属性发生了更改,我们确实希望元素进行动画处理。


3
你为什么选择使用 "isMount" 而不是 "didMount" 或 "isMounted"? - vsync
7
我在考虑当前的渲染是否是发生挂载的那个渲染。是的,我同意,现在回过头来看,这听起来有点奇怪。但在第一次渲染时是正确的,在之后就不正确了,你的建议听起来会误导人。isFirstRender可能可行。 - Scotty Waggoner
5
感谢您的留言!我同意@ScottyWaggoner的观点,"isFirstRender"是一个更好的名称。 - abumalick
1
真棒的东西!! - Alex Dunlop
1
寻找这样的东西已经太久了,太棒了!添加测试是一个很好的点睛之笔。应该是最佳答案! - mattmakesnoise
显示剩余6条评论

38

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

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

这只是一个示例,如果您的情况非常简单,还有其他的做法。

这种方法的缺点是你不能清理效果,只有在依赖数组第二次改变时才会执行。

不建议使用这种方法,您应该使用其他答案提供的方法。但我在这里添加它只是为了让人们知道有多种方法可以做到同样的事情。

编辑:

为了更清楚地说明,您不应该使用这种方法来解决问题(跳过初始渲染),这只是为了教学目的,展示可以用不同的方式做相同的事情。 如果您需要跳过初始渲染,请使用其他答案中提供的方法。


2
我刚学到了一些东西。我没想到这会起作用,但我尝试了一下,它确实有效。谢谢! - zimmerbimmer
2
React应该为此提供更好的解决方案,但是这个问题在Github上仍然存在,建议自己编写一个定制的解决方案(这完全是胡说八道)。 - html_programmer
1
问题在于它也会在unMount时执行,这并不完美。 - Nick Alves
1
@ncesar 如果这是唯一有效的答案,那么很可能你做错了什么,因为其他答案才是正确的,而我的只是为了教学目的。 - Vencovsky

37

我使用普通状态变量而不是 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逻辑重构为自定义hook,像这样。
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 hook以避免额外的重新渲染(感谢@TomEsterez的建议)

这次,我们的自定义hook返回一个函数,该函数返回我们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编程模型。


14
useRef 更适合这种情况,因为 useState 会导致组件额外、无用的重新渲染:https://codesandbox.io/embed/youthful-goldberg-pz3cx - Tom Esterez
你的 useDidMount 函数存在错误。你需要用花括号 { ...}mountRef.current = true 封装在 useEffect() 中。如果不这样做,它就像写了 return mountRef.current = true,这会导致类似于“一个 effect 函数除了用于清理的函数之外,不能返回任何东西。你返回了:true”的错误。 - suther

28

让我向您介绍react-use

npm install react-use

想要在以下情况下运行:

仅在首次渲染后? -------> useUpdateEffect

仅运行一次? -------> useEffectOnce

检查是否是第一次挂载? -------> useFirstMountState

想要使用深层比较浅层比较节流等方式运行效果?等等其他的更多内容,请点击这里

不想安装库吗?可以查看代码 并复制。(也可以为那里的好人们点一个star)

最好的事情就是少一件需要您来维护的事情。


4
看起来是一个非常好的套餐。 - yalcinozer
2
太棒了,我很高兴找到了这个资源! - drichar
以防万一,如果有人在使用React Native或任何不支持树摇的环境,请像这样导入它:import useAsync from 'react-use/lib/useAsync';。详情请见:https://github.com/streamich/react-use/issues/483#issuecomment-514964179。 - cYee
不清楚 useUpdateEffectuseUpdateEffectOnce 之间的区别?两者都只运行一次。 - vsync
这是一个非常臃肿的库,不清楚它是否考虑了树摇优化。如果你只需要在非挂载时运行 useEffect,它还会使 node_modules 变得臃肿... - vsync
显示剩余2条评论

9
一个 TypeScript 和 CRA 友好的 hook,替换它与 useEffect,这个 hook 的工作方式类似于 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)
}

5

这是基于Estus Flask的答案编写的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钩子之间的区别:

Edit 53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render


3

我本来要评论当前被接受的答案,但是空间不够了!

首先,在使用函数组件时,重要的是要摆脱生命周期事件的思维模式,而应该考虑属性/状态变化。我遇到了一个类似的情况,我只希望在特定属性(在我的例子中是parentValue)从其初始状态改变时才触发特定的useEffect函数。因此,我创建了一个基于其初始值的引用:

const parentValueRef = useRef(parentValue);

然后在 useEffect 函数的开头添加以下内容:

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

基本上,如果parentValue没有改变,则不运行效果。如果它已更改,则更新引用,为下一次检查做好准备,并继续运行效果。
因此,尽管其他建议的解决方案将解决您提供的特定用例,但长期来看,改变您对函数组件的思考方式会有所帮助。
将它们视为基于某些道具主要渲染一个组件。
如果您真正需要一些本地状态,则useState将提供该状态,但不要假设存储本地状态可以解决您的问题。
如果您有一些代码会在呈现过程中改变props,则需要将此“副作用”包装在useEffect中,但其目的是具有不受影响的干净渲染正在更改的东西的渲染。 useEffect钩子将在呈现完成后运行,正如您指出的那样,它将在每次呈现时运行-除非使用第二个参数以提供一系列道具/状态来标识什么改变项将导致它在随后运行。
祝您在函数组件/挂钩的旅程中好运!有时必须放弃某些东西才能掌握新的做事方式:) 这是一个很好的入门指南:https://overreacted.io/a-complete-guide-to-useeffect/

2
你可以使用自定义钩子在挂载后运行useEffect。
const useEffectAfterMount = (cb, dependencies) => {
  const mounted = useRef(false);

  useEffect(() => {
    if (mounted.current) {
      return cb();
    }
    mounted.current = true;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

这是 TypeScript 版本:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
  const mounted = useRef(false);

  useEffect(() => {
    if (mounted.current) {
      return cb();
    }
    mounted.current = true;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

例子:

useEffectAfterMount(() => {
  document.title = `You clicked ${count} times`;
}, [count])

你把 true 和 false 弄反了,应该是 const mounted = useRef(false) 然后 mounted.current = true - Mik378

0
下面的解决方案与上面类似,只是我更喜欢这种更简洁的方式。
const [isMount, setIsMount] = useState(true);
useEffect(()=>{
        if(isMount){
            setIsMount(false);
            return;
        }
        
        //Do anything here for 2nd render onwards
}, [args])

2
这不是一个理想的做法,因为设置状态会导致另一个渲染。 - Daniel
引起了一个不必要的重新渲染。使用useRef解决方案更可取。 - vsync

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接