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

Javascript React.StrictMode:useEffect中的SetState函数在效果运行一次时运行多次,javascript,reactjs,react-hooks,next.js,Javascript,Reactjs,React Hooks,Next.js,当oldRunIn未定义时,以下代码的输出与触发效果时的预期一样: 效果正在运行 设置状态正在运行 但是,下次运行useEffect时,定义了状态变量runInArrow,该变量在setState函数中称为oldRunInArrow,输出为: 效果正在运行 设置状态正在运行 设置状态正在运行 设置状态正在运行 为什么效果只运行一次而设置状态运行三次 const [runInArrow, setRunInArrow] = useState<mapkit.PolylineOverlay[] |

当oldRunIn未定义时,以下代码的输出与触发效果时的预期一样:

效果正在运行

设置状态正在运行

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

效果正在运行

设置状态正在运行

设置状态正在运行

设置状态正在运行

为什么效果只运行一次而设置状态运行三次

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]);
这不会运行多次:

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

编辑2:

可复制的例子。点击按钮。您甚至不需要
if(oldState)console.log('oldState')您只需输入第二个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>
  </>;
};
从'react'导入{FunctionComponent,useState,useffect};
导出常量测试:FunctionComponent=函数测试(){
常量[状态,设置状态]=使用状态(未定义);
常量[triggerEffect,setTriggerEffect]=useState(0);
useffect(()=>{
log('effect is running');
设置状态(旧状态=>{
log('setState正在运行');
if(oldState)console.log('oldState');
返回1;
});
},[triggerEffect]);
返回
setTriggerEffect(triggerEffect+1)}type=“button”>触发效果
;
};

编辑3:

我将这段代码放到另一个nextJS项目中,从而复制了这一点。我猜这和nextJS有关


编辑4:

这不是NextJS。它正在以React.Strict模式包装它。这是一本书。为什么?


编辑5:

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

从'react'导入{useState,useffect,useReducer};
功能减速机(状态、数据){
console.log(“减速器正在运行”);
if(state)console.log(state);
返回状态+1;
}
导出常量测试=功能测试(){
const[state,dispatch]=useReducer(减速机,1);
常量[triggerEffect,setTriggerEffect]=useState(0);
useffect(()=>{
调度({});
},[triggerEffect]);
返回(
setTriggerEffect(triggerEffect+1)}type=“button”>
触发效应
);
};
常量Home=()=>(
);
导出默认主页;

如您所知,当您使用React strict模式时会发生这种情况,这是有意的

如图所示:

它运行代码两次 React Strict模式所做的另一件事是运行某些回调/方法两次(仅在开发模式下)。你读对了!以下回调/方法将在严格模式下运行两次(仅在开发模式下):

  • 类组件构造函数方法
  • 渲染方法(包括函数组件)
  • setState更新程序函数(第一个参数)
  • 静态getDerivedStateFromProps生命周期
  • React.useState状态初始值设定项回调函数
  • React.useMemo回调
在钩子回调和类方法中检查哪些日志记录到控制台,以显示某些事情发生两次

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

请注意,即使在dev模式+strict模式下,useffect和useLayoutEffect回调也不会被调用两次,因为这些回调的全部目的是执行副作用

注意,我还注意到,您传递给React.useReducer的reducer在dev模式下不会被调用两次。我不知道为什么会这样,因为我觉得这样的警告也能让我受益

如上所述,包含此行为是为了帮助您发现代码中的错误。由于更新程序函数应该是幂等函数和纯函数,因此运行它们两次而不是一次应该不会对应用程序的功能产生影响。如果是这样,那么这就是代码中的一个bug


听起来您调用的
map.removeOverlays()
方法是一个有效的函数,因此不应该在状态更新程序函数中调用它。我可以理解您为什么根据的答案以这种方式实现它,但我认为使用Chitova 263的答案可以解决这个问题。

正如您所了解的,当您使用React strict模式时,会发生这种情况,这是故意的

如图所示:

它运行代码两次 React Strict模式所做的另一件事是运行某些回调/方法两次(仅在开发模式下)。你读对了!以下回调/方法将在严格模式下运行两次(仅在开发模式下):

  • 类组件构造函数方法
  • 渲染方法(包括函数组件)
  • setState更新程序函数(第一个参数)
  • 静态getDerivedStateFromProps生命周期
  • React.useState状态初始值设定项回调函数
  • React.useMemo回调
在钩子回调和类方法中检查哪些日志记录到控制台,以显示某些事情发生两次

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

N
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>
  </>;
};
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;
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;