当oldRunIn为undefined
时,以下代码的输出与触发效果时的预期一样:
效果正在运行
setState正在运行
但是,下次useEffect在定义了状态变量runInArrow
(在setState函数中称为oldRunInArrow
(的情况下运行时,输出为:
效果正在运行
setState正在运行
setState正在运行
setState正在运行
效果怎么可能只运行一次,而setState却运行三次?
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]);
要理解为什么我使用一个函数来设置状态,请参阅下面的
编辑:
这很奇怪。如果我删除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.SrictMode中。这是一个沙盒。为什么?
编辑5:
正如答案所指出的,这个问题是由于StrictMode故意运行代码两次。它不应该在每个文档中运行useReducer两次(这是我的另一个问题中的"react hook/exhaust deps"的问题(。这是一个UseReducer演示,尝试使用和不使用StrictMode。它也运行两次。似乎它也需要纯粹:
代码沙盒
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;
正如您所了解的,当您使用React严格模式时,就会发生这种情况,而且这是有意的。
如本文所述:
它运行代码两次
React Strict Mode所做的另一件事是运行某些回调/方法两次(仅在DEV模式下(。你没看错!以下回调/方法将在严格模式下运行两次(仅在DEV模式下(:
- 类组件构造函数方法
- 渲染方法(包括功能组件(
- setState更新程序函数(第一个参数(
- 静态getDerivedStateFromProps生命周期
- React.useState状态初始值设定项回调函数
- React.useMemo回调
检查这个代码沙盒,它在钩子回调和类方法中登录到控制台,向您显示某些事情会发生两次。
React这样做是因为它不能可靠地警告您在这些方法中产生的副作用。但是如果这些方法是幂等的,那么多次调用它们应该不会引起任何麻烦。如果它们不是幂等的,那么你应该注意到有趣的事情,你应该能够注意到并修复这些事情。
请注意,即使在开发模式+严格模式下,useEffect和useLayoutEffect回调也不会调用两次,因为这些回调的全部目的是执行副作用。
注意,我还观察到,在开发模式下,传递给React.useReducer的reducer不会被调用两次。我不知道为什么会这样,因为我觉得这也可以从这种警告中受益。
如上所述,包含此行为是为了帮助您查找代码中的错误。由于更新程序函数应该是幂等函数和纯函数,因此运行两次而不是一次应该不会对应用程序的功能产生影响。如果是这样,那么这就是代码中的一个错误。
听起来您正在调用的map.removeOverlays()
方法是一个有效的函数,因此不应该在状态更新器函数中调用它。根据您另一个问题的答案,我可以理解您为什么以这种方式实现它,但我认为使用chitova263的答案可以解决这个问题。
我正在运行这段代码,在组件安装时,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;