React.StrictMode:useEffect中的SetState函数在一次运行效果时会运行多次

时间:2020-02-19 16:34:42

标签: javascript reactjs react-hooks next.js

oldRunIn为undefined时,以下代码的输出与触发效果时预期的一样:

效果正在运行

setState正在运行

但是,下次useEffect运行时定义了状态变量runInArrow,在setState函数中将其称为oldRunInArrow,输出为:

效果正在运行

setState正在运行

setState正在运行

setState正在运行

效果如何只能运行一次而setState可以运行3次?

const [runInArrow, setRunInArrow] = useState<mapkit.PolylineOverlay[] | undefined>(undefined);


useEffect(() => {
      const trueRunIn = settings.magneticRunIn + magVar;

      const boundingRegion = region.toBoundingRegion();
      const style = new mapkit.Style({
        lineWidth: 5,
        lineJoin: 'round',
        strokeColor: settings.runInColor,
      });
      const newRunInArrow = createArrow(boundingRegion, trueRunIn, style);
      console.log('Effect is running');

      setRunInArrow(oldRunIn => {

        // This runs too many times when oldRunIn aka runInArrow is defined

        console.log('setState is running');
        if (oldRunIn) map.removeOverlays(oldRunIn);
        return newRunInArrow;
      });
      map.addOverlays(newRunInArrow);
  }, [magVar, map, mapkit, region, settings.magneticRunIn, settings.runInColor, settings.showRunIn]);

要了解为什么我使用函数来设置状态,请参见此post


编辑:

这很奇怪。如果我删除if (oldRunIn) map.removeOverlays(oldRunIn);,它将按预期工作。但是,如果我将其更改为if (oldRunIn) console.log('oldRunIn is defined');,它将仍然运行多次。我很困惑。

if (oldRunIn) map.addAnnotations([]);不会运行多次。

if (oldRunIn) console.log('test');确实运行了多次。

这总是多次运行(每个使用效果6次):

  setRunInArrow(oldRunIn => {
    console.log('setState is running');
    console.log('test');
    return newRunInArrow;
  });

这不会多次运行:

  setRunInArrow(oldRunIn => {
    console.log('setState is running');
    return newRunInArrow;
  });

编辑2:

可复制的示例。点击按钮。您甚至都不需要if (oldState) console.log('Old state');,只需将其放在第二个console.log中,它将改变行为。

import { FunctionComponent, useState, useEffect } from 'react';

export const Test: FunctionComponent<> = function Test() {
  const [state, setState] = useState<number | undefined>(undefined);
  const [triggerEffect, setTriggerEffect] = useState(0);

  useEffect(() => {
    console.log('effect is running');
    setState(oldState => {
      console.log('setState is runnning');
      if (oldState) console.log('Old state');
      return 1;
    });
  }, [triggerEffect]);

  return <>
    <button onClick={() => setTriggerEffect(triggerEffect + 1)} type="button">Trigger Effect</button>
  </>;
};

编辑3:

我已通过将此代码放入另一个nextJS项目来复制此代码。我猜想它与nextJS有关。


编辑4:

不是NextJS。它包装在React.StrictMode中。这是sandbox。为什么?


编辑5:

正如答案所指出的,该问题是由于StrictMode故意运行两次代码造成的。每个文档不应运行useReducer两次(这是我的另一个问题中的“ react-hooks / exhaustive-deps”问题)。这是UseReducer演示,请尝试使用StrictMode和不使用StrictMode。它还运行两次。似乎它也必须是纯净的:

CodeSandbox

import { useState, useEffect, useReducer } from 'react';

function reducer(state, data) {
  console.log('Reducer is running');
  if (state) console.log(state);
  return state + 1;
}

export const Test = function Test() {
  const [state, dispatch] = useReducer(reducer, 1);
  const [triggerEffect, setTriggerEffect] = useState(0);

  useEffect(() => {
    dispatch({});
  }, [triggerEffect]);

  return (
    <>
      <button onClick={() => setTriggerEffect(triggerEffect + 1)} type="button">
        Trigger Effect
      </button>
    </>
  );
};

const Home = () => (
  <React.StrictMode>
    <Test></Test>
  </React.StrictMode>
);

export default Home;

2 个答案:

答案 0 :(得分:4)

您已经知道,这是在您使用React严格模式时发生的,这是有意的。

this article中所述:

它运行代码TWICE

React Strict Mode所做的另一件事是两次运行某些回调/方法(仅在DEV模式下)。您没看错!以下回调/方法将在严格模式下(仅在开发模式下)运行两次:

  • 类组件构造方法
  • render方法(包括功能组件)
  • setState更新程序功能(第一个参数)
  • 静态getDerivedStateFromProps生命周期
  • React.useState状态初始值设定项回调函数
  • React.useMemo回调

结帐this codesandbox会在钩子回调和类方法中记录到控制台,以向您显示某些事情会发生两次。

React之所以这样做是因为它不能可靠地警告您在这些方法中所产生的副作用。但是,如果这些方法是幂等的,那么多次调用它们就不会造成任何麻烦。如果它们不是幂等的,那么您应该注意到一些有趣的事情,希望它们能够注意到并解决这些问题。

请注意,即使在开发模式+严格模式下,useEffect和useLayoutEffect回调也不会被调用两次,因为这些回调的全部目的是执行副作用。

请注意,我还观察到传递给React.useReducer的reducer在dev模式下不会被调用两次。我不确定为什么会这样,因为我觉得这也可以从这种警告中受益。

如上所示,包含此行为是为了帮助您查找代码中的错误。由于更新程序功能应该是幂等且纯净的,因此运行两次而不是一次应该不会影响应用程序的功能。如果是这样,那么这就是您代码中的错误。

听起来您正在调用的map.removeOverlays()方法是有效的函数,因此不应在状态更新器函数中调用它。我可以看到您为什么基于your other question的答案以这种方式实现它,但是我认为使用chitova263的答案可以解决此问题。

答案 1 :(得分:0)

我正在运行这段代码,并且在组件安装时默认情况下useEffect()最初会被调用一次?

import React, { useState, useEffect } from 'react';

const ChangeDOMTitle = (props) => {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');

    useEffect(() => {
        console.log("Updating the count");
        document.title = `Clicked ${count} times`;
    });

    return (
        <React.Fragment>
            <input type="text" value={name}
             onChange={event => setName(event.target.value)} />
            <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
        </React.Fragment>
    );
};

export default ChangeDOMTitle;