【问题标题】:useEffect is running twice on mount in ReactuseEffect 在 React 中挂载时运行了两次
【发布时间】:2022-09-27 20:24:53
【问题描述】:

我在useEffect 中有一个计数器和一个console.log() 来记录我的状态的每一个变化,但是useEffect 在装载时被调用了两次。我正在使用 React 18。这是我的项目的 CodeSandbox 和下面的代码:

import  { useState, useEffect } from "react";

const Counter = () => {
  const [count, setCount] = useState(5);

  useEffect(() => {
    console.log("rendered", count);
  }, [count]);

  return (
    <div>
      <h1> Counter </h1>
      <div> {count} </div>
      <button onClick={() => setCount(count + 1)}> click to increase </button>
    </div>
  );
};

export default Counter;

【问题讨论】:

标签: javascript reactjs


【解决方案1】:

这是自 React 18 以来的正常行为,当您在 developmentStrictMode 中时。以下是他们在doc 中所说的概述:

将来,我们希望添加一个功能,允许 React 在保留状态的同时添加和删除 UI 部分。

从 React 18 开始使用严格模式,每当组件在开发中安装时,React 将立即模拟卸载和重新安装组件。

在第二次挂载时,React 将从第一次挂载恢复状态。此功能模拟用户行为,例如用户从屏幕上移开并返回,确保代码能够正确处理状态恢复。

这仅适用于development 模式,production 行为不变。

这看起来很奇怪,但最后,它就在那里,所以你可以编写更好的 React 代码,其中每个 useEffect 都有其 clean up 函数,只要有两个调用是一个问题。这里有两个例子:

/* Having a setInterval inside an useEffect: */

import { useEffect, useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setCount((count) => count + 1), 1000);

    /* 
       Make sure I clear the interval when the component is unmounted,
       otherwise I get weird behaviour with StrictMode, 
       helps prevent memory leak issues.
    */
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
};

export default Counter;
/* An API call inside an useEffect with fetch, almost similar with axios: */

useEffect(() => {
  const abortController = new AbortController();

  const fetchUser = async () => {
    try {
      const res = await fetch("/api/user/", {
        signal: abortController.signal,
      });
      const data = await res.json();
    } catch (error) {
      if (error.name === "AbortError") {
        /* 
          Most of the time there is nothing to do here
          as the component is unmounted.
        */
      } else {
        /* Logic for other cases like request failing goes here. */
      }
    }
  };

  fetchUser();

  /* 
    Abort the request as it isn't needed anymore, the component being 
    unmounted. Helps avoid among other things the well known "can't
    perform a React state update on an unmounted component" waring.
  */
  return () => abortController.abort();
}, []);

在这篇名为Synchronizing with Effects 的非常详细的文章中,React 团队以前所未有的方式解释了useEffect,并举了一个例子:

这说明如果重新挂载会破坏应用程序的逻辑,这通常会发现现有的错误.从用户的角度来看,访问一个页面应该与访问它、单击一个链接然后按返回没有什么不同。

React 通过在开发中重新安装组件来验证您的组件不会违反此原则。

对于您的特定用例,您可以不用担心。但是,如果您需要,说您希望 useEffect 的回调仅在 count 更改时运行,您可以使用 booleanuseRef 添加一些额外的控件,如下所示:

import { useEffect, useRef, useState } from "react";

const Counter = () => {
  const countHasChangedRef = useRef(false);
  const [count, setCount] = useState(5);

  useEffect(() => {
    if (!countHasChangedRef.current) return;
    console.log("rendered", count);
  }, [count]);

  return (
    <div>
      <h1>Counter</h1>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
          countHasChangedRef.current = true;
        }}
      >
        Click to increase
      </button>
    </div>
  );
};

export default Counter;

最后,如果您根本不想处理这种development 行为,您可以删除将App 包装在index.jsindex.tsx 中的StrictMode 组件。对于Next.js,删除reactStrictMode: true 内的next.config.js

然而StrictMode 是在development 期间突出潜在问题的工具。并且通常总是有推荐的解决方法,而不是删除它。

【讨论】:

    【解决方案2】:

    为了完成 yousoumar 的回答,React 团队写了一篇关于 useEffect 的帖子,您在其中解释了这一点以及如何正确使用这个钩子: Synchronizing with Effects

    【讨论】:

    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
    【解决方案3】:

    使用ref 或自定义hook 而不使用。

    import type { DependencyList, EffectCallback } from 'react';
    import { useEffect } from 'react';
    
    const useClassicEffect = import.meta.env.PROD
      ? useEffect
      : (effect: EffectCallback, deps?: DependencyList) => {
          useEffect(() => {
            let subscribed = true;
            let unsub: void | (() => void);
    
            queueMicrotask(() => {
              if (subscribed) {
                unsub = effect();
              }
            });
    
            return () => {
              subscribed = false;
              unsub?.();
            };
          }, deps);
        };
    
    export default useClassicEffect;
    

    【讨论】:

      猜你喜欢
      • 2022-06-12
      • 1970-01-01
      • 2020-10-17
      • 1970-01-01
      • 1970-01-01
      • 2020-04-16
      • 2022-08-11
      • 2020-05-21
      相关资源
      最近更新 更多