reactjs useEffect在React中装载时运行两次

bpsygsoo  于 2022-12-03  发布在  React
关注(0)|答案(3)|浏览(306)

我有一个计数器和一个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;
6rvt4ljy

6rvt4ljy1#

这是自React 18以来的正常行为,当你在development中使用StrictMode时。以下是他们在文档中所说的概述:
在未来,我们希望添加一个特性,允许React添加和删除UI的部分,同时保持状态。例如,当用户从一个屏幕切换到另一个屏幕时,React应该能够立即显示上一个屏幕。为此,React将支持使用卸载前使用的相同组件状态重新安装树。
此功能将为React提供更好的开箱即用性能,但要求组件对多次挂载和销毁的效果具有弹性。大多数效果无需任何更改即可工作,但某些效果无法在销毁回调中正确清除订阅,或隐式假定它们仅被挂载或销毁一次。
为了帮助解决这些问题,React 18引入了一个新的仅用于开发的严格模式检查。每当组件第一次装载时,这种新的检查将自动卸载并重新装载每个组件,并在第二次装载时恢复先前的状态。
这仅适用于development模式,production行为不变。
这看起来很奇怪,但最终,它是为了让你编写出更好的React代码,没有错误,符合当前的指导方针,并与未来的版本兼容,通过使用React Query这样的库缓存HTTP请求,并在有两个调用的问题时使用cleanup函数。

/* 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 behavior with StrictMode, 
       helps prevent memory leak issues.
    */
    return () => clearInterval(id);
  }, []);

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

export default Counter;

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

这说明,如果重新挂载破坏了应用程序的逻辑,通常会发现现有的bug。从用户的Angular 来看,访问一个页面应该与访问该页面、单击链接然后按“返回”没有什么不同。React验证了组件在开发过程中重新挂载一次不会破坏这一原则。

对于您的特定用例,您可以让它保持原样而不用担心。而且您不应该尝试在useEffect中使用那些带有useRefif语句的技术来使它触发一次,或者删除StrictMode,如下所示:
React会在开发过程中有意地重新挂载你的组件,以帮助你找到bug。正确的问题不是“如何运行一次效果”,而是“如何修复我的效果,使其在重新挂载后仍能工作”。
通常,答案是实现清理功能。清理功能应该停止或撤销效果正在做的任何事情。经验法则是,用户应该无法区分效果运行一次(如在生产中)和设置→清理→设置序列(如在开发中所见)。

/* As a second example, an API call inside an useEffect with fetch: */

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") {
        /* Logic for non-aborted error handling goes here. */
      }
    }
  };

  fetchUser();

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

dgiusagp2#

**更新:**回头看这篇帖子,稍微明智一点,请不要这样做。

使用ref或自定义不带refhook

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

isr3a4wc3#

(对已接受答案的一个小补充)

仅在第一次装载时应用效果:

const effectRan = useRef(false);

useEffect(() => {
  if (!effectRan.current) {
    console.log("effect ran - only the FIRST time");
  }

  return () => {effectRan.current = true};
}, []);

仅在第二个坐骑上应用效果:

const effectRan = useRef(false);

useEffect(() => {
  if (effectRan.current || process.env.NODE_ENV !== "development") {
    console.log("effect ran - only the SECOND time");
  }

  return () => {effectRan.current = true};
}, []);

相关问题