129

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

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

但是上面的示例将效果应用于初始渲染,以及随后的重新渲染,其中count发生了变化。我如何告诉 React 跳过初始渲染的效果?

4

9 回答 9

169

正如指南所述,

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

于 2018-11-06T21:07:53.530 回答
73

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

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);
  });
});

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

于 2019-05-23T03:31:23.550 回答
29

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

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

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

这样做的缺点是你不能有清理效果,并且只会在依赖数组第二次更改时执行。

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

编辑:

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

于 2020-04-11T13:21:18.340 回答
24

我使用常规状态变量而不是 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 编程模型。

于 2019-03-29T02:13:35.810 回答
8

一个 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)
}
于 2020-05-12T13:02:46.530 回答
5

给大家介绍一下react-use

npm install react-use

想跑:

只有在第一次渲染之后useUpdateEffect

只有一次useEffectOnce

检查它是第一次安装吗?useFirstMountState

想要深度比较浅比较油门运行效果?还有更多

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

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

于 2021-07-31T15:06:03.657 回答
4

这是我基于 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);
}

现场演示

下面的现场演示演示了 betweenuseEffectuseDidUpdateEffecthooks的不同之处

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

于 2021-02-02T17:32:04.977 回答
1

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

const [isMount, setIsMount] = useState(true);
useEffect(()=>{
        if(isMount){
            setIsMount(false);
            return;
        }
        
        //Do anything here for 2nd render onwards
}, [args])
于 2021-01-13T11:57:14.223 回答
0

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

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

const parentValueRef = useRef(parentValue);

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

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

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

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

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

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

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

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

于 2021-02-06T14:21:21.420 回答