使React useEffect挂钩不在初始渲染上运行


106

根据文档:

componentDidUpdate()更新发生后立即调用。初始渲染不调用此方法。

我们可以使用新的useEffect()钩子来模拟componentDidUpdate(),但似乎useEffect()在每次渲染后都被运行,即使是第一次也是如此。如何使它不在初始渲染上运行?

如您在下面的示例中看到的那样,它componentDidUpdateFunction是在初始渲染期间打印的,但componentDidUpdateClass在初始渲染期间没有打印的。

function ComponentDidUpdateFunction() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

class ComponentDidUpdateClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidUpdate() {
    console.log("componentDidUpdateClass");
  }

  render() {
    return (
      <div>
        <p>componentDidUpdateClass: {this.state.count} times</p>
        <button
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          Click Me
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <ComponentDidUpdateFunction />
    <ComponentDidUpdateClass />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


1
我想问一下,根据渲染次数而不是像显式状态变量那样做某件事有意义的用例是什么count
Aprillion '18

Answers:


122

我们可以使用useRef钩子来存储我们喜欢的任何可变值,因此我们可以使用它来跟踪useEffect函数是否是第一次运行。

如果我们希望效果在与该效果相同的阶段中运行componentDidUpdate,则可以使用useLayoutEffect

const { useState, useRef, useLayoutEffect } = React;

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

  const firstUpdate = useRef(true);
  useLayoutEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }

    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

ReactDOM.render(
  <ComponentDidUpdateFunction />,
  document.getElementById("app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


5
我试图取代useRefuseState,但使用的setter引发了重新渲染,分配给时是不会发生firstUpdate.current,所以我想这是唯一的好方法:)
Aprillion

2
如果我们不对DOM进行更改或测量,有人可以解释为什么使用布局效果吗?
ZenVentzi'3

5
@ZenVentzi在此示例中不是必需的,但是问题是如何componentDidUpdate使用钩子进行模拟,因此这就是为什么我使用它。
Tholle

1
我根据此答案在此处创建了一个自定义挂钩。感谢您的实施!
Patrick Roberts

63

您可以将其变成自定义的钩子,如下所示:

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        if (didMount.current) func();
        else didMount.current = true;
    }, deps);
}

export default useDidMountEffect;

用法示例:

import React, { useState, useEffect } from 'react';

import useDidMountEffect from '../path/to/useDidMountEffect';

const MyComponent = (props) => {    
    const [state, setState] = useState({
        key: false
    });    

    useEffect(() => {
        // you know what is this, don't you?
    }, []);

    useDidMountEffect(() => {
        // react please run me if 'key' changes, but not on initial render
    }, [state.key]);    

    return (
        <div>
             ...
        </div>
    );
}
// ...

2
此方法引发警告,指出依赖项列表不是数组文字。
程序员

1
我在项目中使用了此钩子,但没有看到任何警告,您能提供更多信息吗?
Mehdi Dehghani

1
@vsync您正在考虑另一种情况,您希望在初始渲染时运行一次效果,而不再运行
编程人员

2
@vsync在说明部分reactjs.org/docs/...它特别说:“如果你想运行的效果和清理一次(在装载和卸载),你可以传递一个空数组([])作为第二个论点。” 这与我观察到的行为相符。
编程专家

9

我制作了一个简单的useFirstRender钩子来处理诸如着重于表单输入的情况:

import { useRef, useEffect } from 'react';

export function useFirstRender() {
  const firstRender = useRef(true);

  useEffect(() => {
    firstRender.current = false;
  }, []);

  return firstRender.current;
}

它以开头true,然后切换到false中的useEffect,后者仅运行一次,以后不再运行。

在您的组件中,使用它:

const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);

useEffect(() => {
  if (firstRender || errors.phoneNumber) {
    phoneNumberRef.current.focus();
  }
}, [firstRender, errors.phoneNumber]);

对于您的情况,您只需使用即可if (!firstRender) { ...


3

@ravi,您不会调用传入的卸载功能。这是一个更完整的版本:

/**
 * Identical to React.useEffect, except that it never runs on mount. This is
 * the equivalent of the componentDidUpdate lifecycle function.
 *
 * @param {function:function} effect - A useEffect effect.
 * @param {array} [dependencies] - useEffect dependency list.
 */
export const useEffectExceptOnMount = (effect, dependencies) => {
  const mounted = React.useRef(false);
  React.useEffect(() => {
    if (mounted.current) {
      const unmount = effect();
      return () => unmount && unmount();
    } else {
      mounted.current = true;
    }
  }, dependencies);

  // Reset on unmount for the next mount.
  React.useEffect(() => {
    return () => mounted.current = false;
  }, []);
};


您好@Whatabrain,如何在传递非依赖关系列表时使用此自定义钩子?不是一个与componentDidmount相同的空值,而是类似useEffect(() => {...});
KevDing

1
@KevDing应该像dependencies调用它时省略参数一样简单。
Whatabrain

0

@MehdiDehghani,您的解决方案工作得很好,您要做的一项附加工作是卸载,将didMount.current值重置为false。当尝试在其他地方使用此自定义钩子时,您不会获得缓存值。

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        let unmount;
        if (didMount.current) unmount = func();
        else didMount.current = true;

        return () => {
            didMount.current = false;
            unmount && unmount();
        }
    }, deps);
}

export default useDidMountEffect;

2
我不确定这是否有必要,因为如果组件最终没有卸载,因为如果重新安装,那么didMount将已经重新初始化为false
卡梅隆·伊克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.