如何使用 React useEffect 仅调用一次加载函数

How to call loading function with React useEffect only once

提问人:Dávid Molnár 提问时间:11/2/2018 最后编辑:YilmazDávid Molnár 更新时间:11/18/2023 访问量:613832

问:

useEffect React 钩子将在每次更改时运行传入的函数。可以对此进行优化,使其仅在所需属性更改时调用。

如果我想从中调用初始化函数,而不是在更改时再次调用它,该怎么办?假设我想加载一个实体,但加载函数不需要来自组件的任何数据。我们怎样才能使用钩子来做到这一点?componentDidMountuseEffect

class MyComponent extends React.PureComponent {
    componentDidMount() {
        loadDataOnlyOnce();
    }
    render() { ... }
}

使用钩子,这可能如下所示:

function MyComponent() {
    useEffect(() => {
        loadDataOnlyOnce(); // this will fire on every change :(
    }, [...???]);
    return (...);
}
javascript reactjs react-hooks 使用效果

评论


答:

847赞 Tholle 11/2/2018 #1

如果你只想运行初始渲染后给出的函数,你可以给它一个空数组作为第二个参数。useEffect

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

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

评论

76赞 trixn 11/2/2018
或者,如果存在用于获取数据的参数(例如用户 ID),则可以在该数组中传递用户 ID,如果它发生变化,组件将重新获取数据。许多用例都是这样工作的。
7赞 Melounek 11/2/2018
是的。。。有关跳过的更多信息记录在这里:reactjs.org/docs/...
10赞 Simon Hutchison 5/2/2020
这似乎是最简单的答案,但 ESLint 抱怨......查看此线程上的其他答案 stackoverflow.com/a/56767883/1550587
12赞 alexandernst 12/5/2022
注意:从 React 18 开始,这将不再起作用。这段代码将运行两次。
9赞 cd3k 2/1/2023
注意:useEffect 在 React 18 的挂载上运行两次,但仅在开发模式下运行。他们说这是为了帮助调试(但实际上有点令人困惑)。看这里: beta.reactjs.org/reference/react/useEffect#usage
32赞 Yangshun Tay 11/12/2018 #2

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

这告诉 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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

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

195赞 Edan Chetrit 4/3/2019 #3

TL;博士

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

详细说明

useEffect默认情况下,在组件的每次渲染后运行(从而产生效果)。

当放入你的组件时,你告诉 React 你想将回调作为效果运行。React 将在渲染和执行 DOM 更新后运行效果。useEffect

如果仅传递回调 - 回调将在每次渲染后运行。

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

通过向第二个参数传递一个空数组,React 将在每次渲染数组后进行比较,并且会发现没有任何变化,因此仅在第一次渲染后调用回调。

185赞 Ben Carp 6/26/2019 #4

useMountEffect 钩子

在组件挂载后只运行一次函数是一种非常常见的模式,它证明了它自己的钩子是合理的,它隐藏了实现细节。

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

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

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

关于 useMountEffect 钩子

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

评论

36赞 Dynalon 7/24/2019
我非常喜欢你的答案,因为 ESLint 规则“react-hooks/exhaustive-deps”在空依赖项列表中总是会失败。例如,著名的 create-react-app 模板将强制执行该规则。
4赞 HMR 10/9/2019
现在,当你的效果函数需要 props 中的某些东西,但永远不需要再次运行时,即使该值在没有 linter 警告的情况下更改:将缺少依赖项警告,但不会引起警告,但“确实有效”。不过,我不确定并发模式是否会有问题。useMountuseEffect(()=>console.log(props.val),[])useMount(()=>console.log(props.val))
25赞 Vincent Chan 9/4/2020
我不太明白... 仍然抱怨"react-hooks/exhaustive-deps"const useMountEffect = (fun) => useEffect(fun, [])
5赞 Vincent Chan 9/8/2020
谢谢!虽然我认为这指出了 中的一个缺陷,特别是因为这是在 mount 上运行东西的规范方式。这种“解决方案”在功能上将问题从组件转移到其他地方,而不是从根本上解决空 deps 的问题。"react-hooks/exhaustive-deps"
9赞 flyingace 12/2/2020
这不会“绕过”ESLint 规则 b/c,它仍然会指出 useEffect 有一个依赖项:.fun
12赞 ecoologic 10/12/2019 #5

我喜欢定义一个函数,它以同样的方式欺骗 EsLint,我发现它更不言自明。mountuseMount

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

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

6赞 Shuhad zaman 11/16/2020 #6

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

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

空依赖数组仅运行一次,在装载上

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

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

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

无依赖关系。这在每次渲染后都会被调用。

39赞 Yasin Tazeoglu 12/5/2020 #7
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 4/13/2021
谢谢!挽救了我的一天。非常适合调用一次函数,但仅在需要加载某些状态后调用。
0赞 lukasz 11/2/2022
我实际上来到这里是因为我很好奇是否有这样的东西内置了这个 ref 逻辑,所以你不必在组件内部创建一个 ref,但 hook 正在为你处理它?
0赞 Yasin Tazeoglu 11/4/2022
ref 对于条件逻辑是必需的。如果你想在某件事之后运行代码,这个钩子会为你处理它。请检查第二个示例。
4赞 glinda93 8/10/2021 #8

这是我对亚辛答案的版本。

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])
);
86赞 wuarmin 9/21/2021 #9

我们必须停止在组件生命周期方法(即)。我们必须开始思考效果。React 效果不同于旧式的类生命周期方法。componentDidMount

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

如果显式定义效果没有依赖性,则该效果在第一个渲染周期之后仅运行一次。

第一个解决方案(使用 ESLint-complaint)

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

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.

乍一看,这个警告似乎很烦人,但请不要忽视它。它可以帮助您更好地编码,并使您免于“过时的闭包”。如果您不知道什么是“陈旧闭包”,请阅读这篇很棒的文章

第二种解决方案(正确的方法,如果依赖关系不依赖于组件)

如果我们添加到依赖数组中,我们的效果将在每个渲染周期后运行,因为每次渲染时的引用都会发生变化,因为函数被销毁(garbarge-collected)并创建了一个新函数,但这正是我们不想要的。loadDataOnlyOnceloadDataOnlyOnce

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

因此,只需移动上面的函数定义:

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

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

通过此更改,您可以确保 的引用永远不会更改。因此,您还可以安全地添加对依赖项数组的引用。loadDataOnlyOnce

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

如果效果 () 的依赖关系依赖于组件(需要 props 或 state),则有 React 内置的 -Hook。loadDataOnlyOnceuseCallback

-Hook 的基本含义是在渲染周期中保持函数的引用相同。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赞 Gabriel Arghire 10/22/2021
这个答案值得初学者 React 开发人员更多地关注。
6赞 Rodrigo Borba 11/6/2021
@JusticeBringer我想说的不仅仅是初学者。这不是一个简单的概念。
5赞 yalcinozer 11/29/2021
第四个选项。将函数内容直接放入 useEffect 中。这也删除了“eslint”错误。顺便说一句,感谢您解释这个概念。
0赞 armyofda12mnkeys 1/3/2023
何时使用 useEventEffect() 与 useCallback()?
2赞 Mick 1/4/2023
可能值得标记的是,对于在开发模式下使用 create-react-app 的任何人来说,即使是上面应该运行一次的情况,实际上也会运行两次,因为 create-react-app 和严格模型故意双重调用某些函数: stackoverflow.com/a/61091205/334402
1赞 Raydot 11/16/2023
这是我瞥见但无法理解的东西。你把我解决了。谢谢!
-1赞 Vignesh 12/5/2021 #10

如果你只是在渲染后在useeffect中调用该函数,你可以添加一个空数组作为useeffect的第二个参数

useEffect=(()=>{
   functionName(firstName,lastName);
},[firstName,lastName])

calling a custom hooks

adding a empty array for rendering

2赞 jb.darkcharm 1/19/2022 #11

这并不能准确回答您的问题,但可能会产生相同的预期效果,即仅在第一次渲染后运行一次函数。与 componentDidMount 函数非常相似。这使用 useState 而不是 useEffect 来避免依赖项 lint 错误。您只需将一个自执行的匿名函数作为第一个参数传递给 useState。顺便说一句,我不确定为什么 React 不简单地提供一个可以做到这一点的钩子。

import React, { useState } from "react"

const Component = () => {

  useState((() => {
    console.log('componentDidMountHook...')
  }))

  return (
    <div className='component'>Component</div>
  )
}

export default Component
0赞 Greggory Wiley 5/26/2022 #12

即使用户按下后退按钮导航到页面,window.onpageshow 也能正常工作,这与将空数组作为 use-effect 钩子的第二个参数传递不同,后者在通过后退按钮返回页面时不会触发(因此不是在每种形式的初始页面加载上)。

 useEffect(() => {    
     window.onpageshow = async function() {
      setSomeState(false)
      let results = await AsyncFunction() 
       console.log(results, 'Fires on on first load, 
        refresh, or coming to the page via the back button')
    };
 };
12赞 Gnopor 7/12/2022 #13

我在使用 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赞 Dávid Molnár 7/12/2022
您能补充更多细节,为什么这是 React 18 的解决方案吗?
0赞 Gnopor 7/13/2022
对不起,我的意思是我在 React 18 中遇到了这个问题。
1赞 Ali Gajani 6/6/2023
奇怪的是,react-use 的 useEffectOnce 不起作用,但确实如此。
1赞 Vladislav Cherkasheninov 8/8/2022 #14

我在互联网上花了一段时间后才发现。useEffect 在组件挂载时触发一次,然后 componennt 卸载并再次挂载,useEffect 再次触发。你必须在 React 文档上查看更多内容,了解他们为什么要这样做。

所以,我为此使用了自定义钩子。卸载时,您必须更改 useRef 状态。在这种情况下,不要忘记一个 return 语句:当组件卸载时,useEffect 会在返回后运行 cleanup 函数。

import React, { useEffect, useRef } from "react"

const useDidMountEffect = (
  func: () => void,
  deps: React.DependencyList | undefined
) => {
  const didMount = useRef(false)

  useEffect(() => {
    if (didMount.current) {
      func()
    }
    return () => {
      didMount.current = true
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps)
}

export default useDidMountEffect

像正常使用一样使用它 useEffect:

  useDidMountEffect(() => {
    // your action    
  }, [])
0赞 Andrei Belokopytov 10/11/2022 #15

我发现,用这个功能从问题可以简洁而优雅地解决。oncelodash

import { once } from "lodash";
import { useEffect, useRef } from "react";

export const useEffectOnce = (cb: () => void) => {
  const callBackOnce = useRef(once(cb)).current;
  useEffect(() => callBackOnce(), [callBackOnce]);
};
0赞 phikes 5/30/2023 #16

将来我们会有,看这篇官方的react文章useEffectEvent

使用一个名为 useEffectEvent 的特殊 Hook 从 Effect 中提取此非反应式逻辑:

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

在这里,onConnected 称为 Effect Event。它是效果逻辑的一部分,但它的行为更像一个事件处理程序。它里面的逻辑不是被动的,它总是“看到”你的道具和状态的最新值。

在他们的示例中,即使更改了(或问题情况下的回调),效果事件也不会再次运行(即 没有任何依赖项)。themeuseEffectEvent

因此,要回答这个问题(假设依赖于某些 props/state 或作用域中的其他变量,它们会一次又一次地执行它;否则只需将其放在组件主体之外;针对流控制做出反应建议):loadDataOnlyOnceuseCallback

function MyComponent() {
    const loadDataOnlyOnceEvent = useEffectEvent(loadDataOnlyOnce)

    useEffect(() => {
        loadDataOnlyOnceEvent();
    }, []);
    return (...);
}
0赞 Akeel Ahmed Qureshi 11/8/2023 #17

你在useEffect中传递空白数组,所以它运行一次

import React, { useEffect } from "react";

function MyComponent() {
  useEffect(() => {
    // This code will run only once when the component mounts
  }, []); // The empty dependency array means the effect runs only once

  return <div>{/* Your component JSX */}</div>;
}
1赞 Aryan kholqi 11/18/2023 #18

您应该执行以下第一种方式

useEffect 钩子有 3 种使用方式。

  1. 在组件将呈现时运行一次:

     useEffect(()=>{
         // your code
     },[])
    
  2. 每次运行这些 x1,x2,...依赖项已被查格:

     useEffect(()=>{
     // your code
     },[x1,x2,...])
    
  3. 在更新任何内容以及渲染组件时运行:

      useEffect(()=>{
     // your code
      })