如何在React的useEffect中仅调用一次加载函数

524
useEffect React钩子将在每次更改时运行传入的函数。这可以优化为仅在期望的属性更改时调用它。
如果我想从componentDidMount调用初始化函数而不在更改时再次调用它怎么办?假设我想要加载一个实体,但是加载函数不需要组件中的任何数据。我们如何使用useEffect钩子来实现这一点?
class MyComponent extends React.PureComponent {
    componentDidMount() {
        loadDataOnlyOnce();
    }
    render() { ... }
}

使用Hooks可以让它看起来像这样:

function MyComponent() {
    useEffect(() => {
        loadDataOnlyOnce(); // this will fire on every change :(
    }, [...???]);
    return (...);
}
18个回答

847

如果你只想在初始渲染后运行给定给useEffect的函数,你可以将空数组作为第二个参数传递。

function MyComponent() {
  useEffect(() => {
    loadDataOnlyOnce();
  }, []);

  return <div> {/* ... */} </div>;
}

76
如果有参数用于获取数据(例如用户 ID),您可以在该数组中传递用户 ID,如果更改,则组件将重新获取数据。许多用例将按此方式工作。 - trixn
7
好的,关于跳过效果的更多信息在这里记录:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects - Melounek
10
这似乎是最简单的答案,但 ESLint 抱怨了... 请查看本线程上的其他答案 https://dev59.com/j1QJ5IYBdhLWcg3wx44V#56767883 - Simon Hutchison
12
注意:从 React 18 开始,这将不再起作用。这段代码会运行两次。 - alexandernst
9
注意:在React 18中,useEffect在挂载时会运行两次,但仅在开发模式下。他们说这是为了帮助调试(但实际上有点令人困惑)。 请参见:https://beta.reactjs.org/reference/react/useEffect#usage - cd3k
显示剩余9条评论

195

TL;DR

useEffect(yourCallback, []) - 仅在第一次渲染后触发回调函数。

详细解释

useEffect 默认在组件每次渲染后运行(从而导致效果)。

当向组件中添加useEffect时,您告诉React要将回调函数作为效果运行。 React将在渲染和执行DOM更新之后运行该效果。

如果只传递一个回调函数,则该回调函数将在每次渲染后运行。

如果传递第二个参数(数组),则React将在第一次渲染后运行回调,并在更改数组中的任何元素时运行回调。例如,当放置useEffect(() => console.log('hello'), [someVar, someOtherVar])时,回调将在第一次渲染后以及其中一个someVarsomeOtherVar更改时运行。

通过传递空数组作为第二个参数,React会在每次渲染后比较数组,并且会发现没有任何更改,因此仅在第一次渲染后调用回调函数。


185

useMountEffect 钩子

在组件挂载后仅运行一次函数是一个常见模式,它证明了需要一个钩子来隐藏实现细节。

const useMountEffect = (fun) => useEffect(fun, [])

在任何功能组件中使用它。

function MyComponent() {
    useMountEffect(function) // function will run only once after it has mounted. 
    return <div>...</div>;
}

关于 useMountEffect 钩子的使用

当在 useEffect 中使用第二个数组参数时,React 将在挂载(初始渲染)后和数组中的值发生更改后运行回调函数。由于我们传递了一个空数组,因此它只会在挂载后运行。


36
我更倾向于你的答案,因为 ESLint 规则 "react-hooks/exhaustive-deps" 在空依赖列表上始终会失败。例如,著名的 create-react-app 模板将强制执行该规则。 - Dynalon
4
现在,当您的效果函数需要从props中获取某些内容但不需要再次运行甚至在该值更改时,您可以使用useMount而不会受到linter警告。例如,useEffect(()=>console.log(props.val),[])将会有缺失依赖项警告,但是useMount(()=>console.log(props.val))不会产生警告,但仍然能正常工作。但我不确定在并发模式下是否会出现问题。 - HMR
25
我不太明白... "react-hooks/exhaustive-deps" 仍然抱怨在 const useMountEffect = (fun) => useEffect(fun, []) 中的空数组。 - Vincent Chan
5
谢谢!不过我认为这指向了“react-hooks/exhaustive-deps”中的一个缺陷,特别是这是在挂载组件时运行事物的规范方式。这个“解决方案”实际上是将问题从组件移动到其他地方,而不是从根本上解决空依赖项的问题。 - Vincent Chan
9
这并不会“规避”ESLint规则,因为它仍然会指出useEffect有一个依赖项:fun - flyingace
显示剩余17条评论

86

我们必须停止思考组件生命周期方法(例如componentDidMount)。我们必须开始思考副作用。React的效果与旧式类生命周期方法不同。

默认情况下,每次渲染周期后都会运行效果,但有选项可以选择退出此行为。要选择退出,可以定义依赖项,这意味着只有在对其中一个依赖项进行更改时才执行该效果。

如果您明确定义了一个效果没有任何依赖关系,那么该效果仅在第一个渲染周期后运行一次。

第1个解决方案(带有ESLint警告)

因此,您的示例的第一个解决方案如下:

function MyComponent() {

    const loadDataOnlyOnce = () => {
      console.log("loadDataOnlyOnce");
    };

    useEffect(() => {
        loadDataOnlyOnce(); // this will fire only on first render
    }, []);
    return (...);
}

但 React Hooks ESLint 插件可能会提示类似以下的错误:

React Hook useEffect has missing dependency: loadDataOnlyOnce. Either include it or remove the dependency array

起初这个警告看起来很烦人,但请不要忽略它。它有助于你更好地编写代码并避免“闭包陈旧”问题。如果你不知道什么是“闭包陈旧”,请阅读这篇优秀的文章.

第二种解决方案(如果依赖项不依赖于组件,则为正确方法)

如果我们将 loadDataOnlyOnce 添加到依赖项数组中,则我们的 effect 将在每个渲染周期后运行,因为在每次渲染时,loadDataOnlyOnce 的引用都会更改。这是因为该函数被销毁(垃圾回收),并创建了一个新的函数,但这恰恰不是我们想要的。

我们必须在渲染周期中保持相同的loadDataOnlyOnce 引用。

所以只需要将函数定义移到上面即可:

const loadDataOnlyOnce = () => {
  console.log("loadDataOnlyOnce");
};

function MyComponent() {
    useEffect(() => {
        loadDataOnlyOnce(); // this will fire only on first render
    }, [loadDataOnlyOnce]);
    return (...);
}

通过这个更改,您可以确保loadDataOnlyOnce的引用永远不会改变。因此,您也可以将引用安全地添加到依赖项数组中。

第三种解决方案(如果依赖于组件,则为正确方法)

如果效果的依赖性(loadDataOnlyOnce)依赖于组件(需要props或state),则React内置了useCallback钩子。

useCallback钩子的基本意义是在渲染周期内保持函数的引用相同。

function MyComponent() {
    const [state, setState] = useState("state");

    const loadDataOnlyOnce = useCallback(() => {
      console.log(`I need ${state}!!`);
    }, [state]);

    useEffect(() => {
        loadDataOnlyOnce(); // this will fire only when loadDataOnlyOnce-reference changes
    }, [loadDataOnlyOnce]);
    return (...);
}

16
这个答案值得初学者 React 开发者更多的关注。 - Gabriel Arghire
6
@JusticeBringer 我认为这不仅适用于初学者。这个概念并不是很简单易懂的。 - Rodrigo Borba
5
把函数内容直接放进 useEffect 中,这样可以消除“eslint”错误。顺便感谢你解释这个概念。第四个选项。 - yalcinozer
何时使用useEventEffect()和useCallback()? - armyofda12mnkeys
@armyofda12mnkeys useEventCallback 不是官方的 React Hook。 - wuarmin
2
值得注意的是,对于在开发模式下使用create-react-app的任何人来说,即使上述应该只运行一次的情况也会实际运行两次,因为create-react-app和严格模式有意双重调用某些函数:https://dev59.com/XOk5XIcBkEYKwwoY7eME#61091205 - Mick

39
function useOnceCall(cb, condition = true) {
  const isCalledRef = React.useRef(false);

  React.useEffect(() => {
    if (condition && !isCalledRef.current) {
      isCalledRef.current = true;
      cb();
    }
  }, [cb, condition]);
}

并使用它。

useOnceCall(() => {
  console.log('called');
})
或者
useOnceCall(()=>{
  console.log('Fetched Data');
}, isFetched);

1
谢谢!救了我的一天。非常适合在需要加载某些状态之后仅调用函数一次的情况下使用。 - Davit
我之所以来到这里,是因为我很好奇是否有这样一种东西,它内置了这个 ref 逻辑,这样你就不必在组件内部创建一个 ref,而是由 hook 来为你处理? - lukasz
条件逻辑需要引用 ref。如果您想在某个操作之后运行代码,则此钩子将为您处理。请查看第二个示例。 - Yasin Tazeoglu

32
将空数组作为第二个参数传递给useEffect。这有效地告诉React,引用文档

这告诉React您的效果不依赖于props或state中的任何值,因此它永远不需要重新运行。

以下是一个可以运行以演示其工作原理的代码段:

function App() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUser(data.results[0]);
      });
  }, []); // Pass empty array to only run once on mount.
  
  return <div>
    {user ? user.name.first : 'Loading...'}
  </div>;
}

ReactDOM.render(<App/>, 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>


12

我喜欢定义一个mount函数,它与useMount的工作方式相同,可以欺骗EsLint,而且我认为它更易于理解。

const mount = () => {
  console.log('mounted')
  // ...

  const unmount = () => {
    console.log('unmounted')
    // ...
  }
  return unmount
}
useEffect(mount, [])


12

我在使用React 18时遇到了这个问题。这是我的解决方法:

import { useEffect, useRef } from "react";

export default function Component() {
    const wasCalled = useRef(false);

    useEffect(() => {
        if(wasCalled.current) return;
        wasCalled.current = true;
            
         /* CODE THAT SHOULD RUN ONCE */

    }, []);

    return <div> content </div>;
}

在这里查看为什么useEffect会被调用多次的解释。


1
你能否提供更多细节,说明为什么这是 React 18 的解决方案? - Dávid Molnár
抱歉,我的意思是我在React 18中遇到了这个问题。 - Gnopor
1
奇怪的是,react-use中的useEffectOnce不起作用,但这个可以。 - Ali Gajani
1
奇怪的是,react-use 中的 useEffectOnce 不起作用,但这个却可以。 - undefined

6

将依赖数组留空。希望这能帮助你更好地理解。

   useEffect(() => {
      doSomething()
    }, []) 

空的依赖数组只在挂载时执行一次

useEffect(() => {
  doSomething(value)
}, [value])  

value 作为依赖项传递。如果自上次以来依赖项已更改,则效果将再次运行。

useEffect(() => {
  doSomething(value)
})  

无依赖。每次呈现后都会调用此函数。


4

这是我对 Yasin 答案的版本。

import {useEffect, useRef} from 'react';

const useOnceEffect = (effect: () => void) => {
  const initialRef = useRef(true);

  useEffect(() => {
    if (!initialRef.current) {
      return;
    }
    initialRef.current = false;
    effect();
  }, [effect]);
};

export default useOnceEffect;

使用方法:

useOnceEffect(
  useCallback(() => {
    nonHookFunc(deps1, deps2);
  }, [deps1, deps2])
);

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