在初始渲染时使 React useEffect 钩子不 运行

Make React useEffect hook not run on initial render

根据文档:

componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.

我们可以使用新的 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>

We can use the useRef hook to store any mutable value we like,因此我们可以使用它来跟踪 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>

你可以把它变成custom hooks,像这样:

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

@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;

@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;
  }, []);
};

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

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

它开始时是 true,然后在 useEffect 中切换到 false,它只运行一次,再也不会运行。

在你的组件中,使用它:

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

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

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

这是迄今为止我使用 typescript 创建的最佳实现。基本上,想法是一样的,使用 Ref 但我也在考虑 useEffect 返回的回调来执行组件卸载的清理。

import {
  useRef,
  EffectCallback,
  DependencyList,
  useEffect
} from 'react';

/**
 * @param effect 
 * @param dependencies
 *  
 */
export default function useNoInitialEffect(
  effect: EffectCallback,
  dependencies?: DependencyList
) {
  //Preserving the true by default as initial render cycle
  const initialRender = useRef(true);

  useEffect(() => {
    let effectReturns: void | (() => void) = () => {};

    // Updating the ref to false on the first render, causing
    // subsequent render to execute the effect
    if (initialRender.current) {
      initialRender.current = false;
    } else {
      effectReturns = effect();
    }

    // Preserving and allowing the Destructor returned by the effect
    // to execute on component unmount and perform cleanup if
    // required.
    if (effectReturns && typeof effectReturns === 'function') {
      return effectReturns;
    } 
    return undefined;
  }, dependencies);
}

您可以简单地使用它,就像您使用 useEffect 挂钩一样,但这次,它不会 运行 在初始渲染中。下面是如何使用这个钩子。

useuseNoInitialEffect(() => {
  // perform something, returning callback is supported
}, [a, b]);

如果您使用 ESLint 并希望对此自定义挂钩使用 react-hooks/exhaustive-deps 规则:

{
  "rules": {
    // ...
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "useNoInitialEffect"
    }]
  }
}

相同的方法,但使用 useState 而不是 useRef

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

...

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

编辑

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

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

如果你想跳过第一个渲染,你可以创建一个状态“firstRenderDone”并在具有空依赖列表的useEffect中将它设置为true(就像didMount一样工作)。然后,在你的另一个useEffect中,你可以在做某事之前检查第一次渲染是否已经完成。

const [firstRenderDone, setFirstRenderDone] = useState(false);

//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
  setFirstRenderDone(true);
}, []);

// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
  if(firstRenderDone){
    console.log("componentDidUpdateFunction");
  }
}, [firstRenderDone]);

所有前面的都很好,但是考虑到可以“跳过”useEffect 中的操作放置基本上不是 运行 第一次的 if 条件(或任何其他),这可以通过更简单的方式实现, 并且仍然具有依赖性。

例如我有这样的情况:

  1. 从 API 加载数据,但我的标题必须是“正在加载”,直到日期不存在,所以我有一个数组,开始时是空的,并显示文本“正在显示”。 =25=]
  2. 使用与 API 不同的信息渲染组件。
  3. 用户可以将这些信息一个一个地删除,甚至全部清空旅游数组作为开始,但这次 API 提取已经完成
  4. 一旦通过删除游览列表为空,然后显示另一个标题。

所以我的“解决方案”是创建另一个 useState 以创建一个布尔值,该值仅在数据获取后更改,使 useEffect 中的另一个条件为真,以便 运行 另一个也取决于游览长度的函数.

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])

这是我的 App.js

import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'

const url = 'API url'

let newTours

function App() {
  const [loading, setLoading ] = useState(true)
  const [tours, setTours] = useState([])
  const [isTitle, isSetTitle] = useState(false)
  const [title, setTitle] = useState("Our Tours")

  const newTitle = "Tours are empty"

  const removeTours = (id) => {
    newTours = tours.filter(tour => ( tour.id !== id))

    return setTours(newTours)
  }

  const changeTitle = (title) =>{
    if(tours.length === 0 && loading === false){
      setTitle(title)
    }
  }

const fetchTours = async () => {
  setLoading(true)

  try {
    const response = await fetch(url)
    const tours = await response.json()
    setLoading(false)
    setTours(tours)
  }catch(error) {
    setLoading(false)
    console.log(error)
  }  
}


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

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])


if(loading){
  return (
    <main>
      <Loading />
    </main>
  )  
}else{
  return ( 

    <main>
      <Tours tours={tours} title={title} changeTitle={changeTitle}           
removeTours={removeTours} />
    </main>
  )  
 }
}



export default App

一种简单的方法是在您的组件之外创建一个 let,并将其设置为 true。

然后说如果它的 true 将它设置为 false 然后 return(停止)useEffect 函数

像那样:


    import { useEffect} from 'react';
    //your let must be out of component to avoid re-evaluation 
    
    let isFirst = true
    
    function App() {
      useEffect(() => {
          if(isFirst){
            isFirst = false
            return
          }
    
        //your code that don't want to execute at first time
      },[])
      return (
        <div>
            <p>its simple huh...</p>
        </div>
      );
    }

它类似于@Carmine Tambasciabs 解决方案,但不使用状态 :) ‍‍‍‍‍‍ ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

简化实施

import { useRef, useEffect } from 'react';

function MyComp(props) {

  const firstRender = useRef(true);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
    } else {
      myProp = 'some val';
    };

  }, [props.myProp])


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

}

保持简单:

function useEffectAfterFirstRender(effect, deps) {
  const isFirstRender = useRef(true);

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

如果您消除不必要的并发症,此处的其他解决方案将简化为:

  • 我们需要传递 effect() 的 return 值,因为它可能是 析构函数,但我们不需要做任何条件逻辑来 确定它是否是。把它传下去,不管它是什么,然后让 useEffect 搞清楚。
  • 在卸载时将 isFirstRender 重置为 true 没有意义,因为 1) 条件未变为 true,并且 2) 在卸载时,ref 将进入焚化炉。它不会在“下一个安装”上重复使用。没有下一个坐骑。卸载就是死亡。

这是一个完整的打字稿模块:

import { useEffect, useRef, EffectCallback, DependencyList } from 'react';

function useEffectAfterFirstRender(effect: EffectCallback, deps: DependencyList): void {
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) isFirstRender.current = false;
    else return effect();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
}

export default useEffectAfterFirstRender;

我赞成 Kiran Maniya 的建议,给它一个 exhaustive-deps eslint 规则:

{
  "rules": {
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "useEffectAfterFirstRender"
    }]
  }
}

我认为创建一个自定义挂钩会有点矫枉过正,我不想通过使用 useLayoutEffect 挂钩来处理与布局无关的内容来混淆我的组件的可读性,因此,就我而言,我只是检查了查看触发 useEffect 回调的状态变量 selectedItem 的值是否是其原始值,以确定它是否是初始渲染:

export default function MyComponent(props) {
    const [selectedItem, setSelectedItem] = useState(null);

    useEffect(() => {
        if(!selectedItem) return; // If selected item is its initial value (null), don't continue
        
        //... This will not happen on initial render

    }, [selectedItem]);

    // ...

}