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

Yan*_*Tay 31 javascript reactjs react-hooks

根据文件:

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")
);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

Tho*_*lle 47

我们可以使用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")
);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 最近的 React18 在开发模式下调用 usEffect 两次,您需要添加一个 cleanUp 函数,以便防止在第一次渲染上运行继续起作用: `useLayoutEffect(() =&gt; { if (firstUpdate.current) { firstUpdate. current = false; return; } console.log("componentDidUpdateFunction"); return () =&gt; (firstUpdate.current = true); });` (7认同)
  • 我试图用 `useState` 替换 `useRef`,但是使用 setter 触发了重新渲染,这在分配给 `firstUpdate.current` 时不会发生,所以我想这是唯一的好方法:) (5认同)
  • 如果我们不改变或测量 DOM,有人可以解释为什么要使用布局效果吗? (4认同)
  • @ZenVentzi在这个例子中不是必须的,但是问题是如何用钩子模拟`componentDidUpdate`,所以这就是为什么我使用它。 (3认同)

Mar*_*ais 33

我做了一个简单的useFirstRender钩子来处理像聚焦表单输入这样的情况:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}
Run Code Online (Sandbox Code Playgroud)

它以 开头true,然后切换到falseuseEffect,它只运行一次,再也不会运行。

在您的组件中,使用它:

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

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

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

  • 如果将“firstRender”添加到 useEffect 中的依赖项数组中,则在挂载时将运行两次(第一次,当firstRender 设置为 false 时)。我从代码中的依赖项中删除了它并且它起作用了。 (2认同)

Lui*_*igi 17

与Tholle 的答案相同的方法,但使用useState而不是useRef.

const [skipCount, setSkipCount] = useState(true);

...

useEffect(() => {
    if (skipCount) setSkipCount(false);
    if (!skipCount) runYourFunction();
}, [dependencies])

Run Code Online (Sandbox Code Playgroud)

编辑

虽然这也有效,但它涉及更新状态,这将导致您的组件重新渲染。如果您的所有组件的useEffect调用(以及它的所有子组件)都有一个依赖数组,那么这并不重要。但请记住,任何useEffect没有依赖项数组的 (useEffect(() => {...})都将再次运行。

使用和更新useRef不会导致任何重新渲染。


Meh*_*ani 8

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

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;
Run Code Online (Sandbox Code Playgroud)

用法示例:

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>
    );
}
// ...
Run Code Online (Sandbox Code Playgroud)

  • 这种方法会抛出警告,指出依赖项列表不是数组文字。 (4认同)
  • 您只需要“return func()”将返回值发送到“useEffect”,以防它是析构函数(即清理函数)。 (3认同)
  • @vsync您正在考虑另一种情况,您希望在初始渲染时运行一次效果,然后不再运行 (2认同)
  • @vsync 在 https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects 的注释部分中,它特别指出“如果您只想运行效果并清理它一旦(在安装和卸载时),您可以传递一个空数组([])作为第二个参数。” 这与我观察到的行为相符。 (2认同)

Wha*_*ain 7

@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;
  }, []);
};
Run Code Online (Sandbox Code Playgroud)


sko*_*kot 6

function useEffectAfterMount(effect, deps) {
  const isMounted = useRef(false);

  useEffect(() => {
    if (isMounted.current) return effect();
    else isMounted.current = true;
  }, deps);

  // reset on unmount; in React 18, components can mount again
  useEffect(() => () => {
    isMounted.current = false;
  }, []);
}
Run Code Online (Sandbox Code Playgroud)

我们需要返回从effect(),因为它可能是一个清理函数。但我们不需要确定它是与否。把它传递出去,让我们useEffect弄清楚。

在这篇文章的早期版本中,我说过重置 ref ( isMounted.current = false) 是不必要的。但在 React 18 中确实如此,因为组件可以以其之前的状态重新挂载(感谢 @Whatabrain)。