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

With useEffect, how can I skip applying an effect upon the initial render?

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

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

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

如指南所述,

The Effect Hook, useEffect, adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single 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

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

// 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 的建议)

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

这是一个自定义挂钩,它只提供一个布尔标志来指示当前渲染是否是第一个渲染(当组件被挂载时)。它与其他一些答案大致相同,但您可以在 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);
  });
});

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

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

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

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

这样做的缺点是不能起到清理作用,只有在依赖数组第二次变化时才会执行

不建议使用,您应该使用其他答案所说的内容,但我只是在此处添加此内容,以便人们知道有不止一种方法可以做到这一点。

编辑:

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

一个 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)
}

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

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

这是我基于 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 钩子之间的区别

我本来打算对当前接受的答案发表评论,但 运行 出 space!

首先,重要的是在使用功能组件时不要考虑生命周期事件。考虑 prop/state 变化。我有一个类似的情况,我只希望在特定道具(parentValue 在我的例子中)从其初始状态发生变化时触发特定的 useEffect 函数。所以,我创建了一个基于其初始值的 ref:

const parentValueRef = useRef(parentValue);

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

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

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

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

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

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

如果你有一些代码会在渲染过程中改变你的道具,这个 'side-effect' 需要包装在 useEffect 中,但这样做的目的是有一个干净的渲染在渲染时受到某些变化的影响。渲染完成后 useEffect 挂钩将是 运行,正如您所指出的,每次渲染都是 运行 - 除非第二个参数用于提供 运行 列表=42=] 来确定哪些更改的项目将导致它成为 运行 随后的时间。

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

给大家介绍一下react-use.

npm install react-use

想要运行:

仅在第一次渲染后? ------> useUpdateEffect

只有一次? ------> useEffectOnce

检查是否先挂载? ------> useFirstMountState

想要运行效果与深度比较浅层比较节流?还有更多 here.

不想安装库?检查 code 并复制。 (也许 star 也适合那里的好人)

最好的事情是 您需要维护的东西少了一件。