【问题标题】:Make React useEffect hook not run on initial render使 React useEffect 钩子不在初始渲染时运行
【发布时间】:2019-04-14 16:31:42
【问题描述】:

根据文档:

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

【问题讨论】:

  • 我可以问一下,当基于渲染次数而不是像 count 这样的显式状态变量来做某事是有意义的时候,用例是什么?
  • @Aprillion,在我的情况下,更改 H2 的内容,其中包含需要在项目列表之后更改的文本,它是空的,并且在开始时甚至不同。在从 API 获取数据之前,相同的列表在开始时也是空的,因此对于基于数组长度的正常条件渲染,初始值会被覆盖

标签: javascript reactjs react-hooks


【解决方案1】:

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>

【讨论】:

  • 我尝试用useState 替换useRef,但是使用setter 触发了重新渲染,这在分配给firstUpdate.current 时不会发生,所以我想这是唯一的好方法: )
  • 如果我们不改变或测量 DOM,有人能解释一下为什么要使用布局效果吗?
  • @ZenVentzi 在这个例子中没有必要,但问题是如何用钩子模仿componentDidUpdate,所以这就是我使用它的原因。
  • 我根据这个答案创建了一个自定义钩子here。感谢您的实施!
【解决方案2】:

你可以把它变成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>
    );
}
// ...

【讨论】:

  • 这种方法会抛出警告,说依赖列表不是数组文字。
  • 我在我的项目中使用了这个钩子,我没有看到任何警告,你能提供更多信息吗?
  • @vsync 您正在考虑另一种情况,您希望在初始渲染时运行一次效果,然后再也不运行
  • @vsync 在reactjs.org/docs/… 的注释部分,它特别指出“如果你想运行一个效果并且只清理一次(在挂载和卸载时),你可以传递一个空数组([ ]) 作为第二个论点。”这符合我观察到的行为。
  • 我还收到警告,因为依赖列表不是数组文字并且缺少依赖:'func'。两者的 linter 规则是 react-hooks/exhaustive-deps
【解决方案3】:

我做了一个简单的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) { ...

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 你好@Whatabrain,如何使用这个自定义钩子传递非依赖列表?不是空的,它与 componentDidmount 相同,但类似于 useEffect(() =&gt; {...});
    • @KevDing 应该就像在调用时省略dependencies 参数一样简单。
    【解决方案5】:

    Tholle's answer 相同的方法,但使用useState 而不是useRef

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

    编辑

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

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

    【讨论】:

    • 这是一个很好的解决方案,让我通过了我的解决方案
    【解决方案6】:

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

    【讨论】:

    • 我不确定这是否有必要,因为如果组件最终卸载,因为如果它重新安装,didMount 将已经重新初始化为false
    • @CameronYick 这是必要的,因为当使用快速刷新时,清理功能将运行,但参考值不会被清除。这会导致开发时出现故障 - 但在生产中会很好。
    【解决方案7】:

    这是迄今为止我使用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]);
    

    【讨论】:

      【解决方案8】:

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

      例如我遇到的情况:

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

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

      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
      

      【讨论】:

      • 我看不出这种方法有什么不同,除了 if 语句中条件的顺序。
      • @Luigi 与您的 if 检查相同的变量并不完全相同,因此运行一个或另一个,在我的用例中,首先如果更改标题变量,第二个将状态变量设置回真的
      • 这就是我改变订单的意思。你检查你之前是否设置过它,如果有,你运行你的函数。我的首先检查我是否还没有设置它,然后设置它。否则,它会运行我的函数(在你的情况下,changeTitle)。
      【解决方案9】:

      如果你想跳过第一次渲染,你可以创建一个状态“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]);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-12-27
        • 1970-01-01
        • 1970-01-01
        • 2021-05-18
        • 1970-01-01
        • 2021-12-15
        • 2021-01-06
        相关资源
        最近更新 更多