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

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

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

如果使用钩子,可能是这样的:

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

当前回答

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将在挂载(初始渲染)和数组中的值发生变化后运行回调。因为我们传递了一个空数组,所以它只会在挂载之后运行。

其他回答

窗口。即使用户按下返回按钮导航到页面,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')
    };
 };

我发现使用lodash中的once函数可以简洁而优雅地解决这个问题。

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

export const useEffectOnce = (cb: () => void) => {
  const callBackOnce = useRef(once(cb)).current;
  useEffect(() => callBackOnce(), [callBackOnce]);
};

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

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

如果显式地定义一个效果没有依赖关系,则该效果只在第一个渲染循环之后运行一次。

第一个解决方案(带有ESLint-complaint)

所以,你的例子的第一个解决方案是:

function MyComponent() {

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

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

但是React Hooks ESLint插件会抱怨这样的事情:

React钩子useEffect缺少依赖项:loadDataOnlyOnce。要么包含它,要么删除依赖数组。

起初,这个警告似乎很烦人,但请不要忽视它。它可以帮助你更好地编码,并将你从“陈旧的闭包”中拯救出来。如果你不知道什么是“过期闭包”,请阅读这篇很棒的文章。

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

如果我们将loadDataOnlyOnce添加到依赖数组中,我们的效果将在每次呈现循环后运行,因为loadDataOnlyOnce的引用在每次呈现时都会改变,因为函数被销毁(垃圾回收)并创建一个新函数,但这正是我们不想要的。

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

所以只要把函数定义移到上面:

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

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

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

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

如果效果的依赖关系(loadDataOnlyOnce)依赖于组件(需要道具或状态),React内置的useCallback-Hook。

useCallback-Hook的基本含义是在呈现循环期间保持函数的引用相同。

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

以下是我对亚辛回答的看法。

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

这并没有完全回答你的问题,但可能会产生只运行一次函数的预期效果,并且是在第一次呈现之后。非常类似于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