The output of the code below when oldRunIn is undefined
is as expected when the effect is triggered:
Effect is running
setState is running
However, the next time useEffect runs with the state variable runInArrow
defined, which is referred to as oldRunInArrow
in the setState function the output is:
Effect is running
setState is running
setState is running
setState is running
How is it possible that the effect runs only one time but the setState runs 3 times?
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]);
To understand why I'm using a function to set state see this post
Edit:
This is strange. if I remove if (oldRunIn) map.removeOverlays(oldRunIn);
it works as expected. However if I change it to if (oldRunIn) console.log('oldRunIn is defined');
it still runs multiple times. I'm thoroughly confused.
if (oldRunIn) map.addAnnotations([]);
does not run multiple times.
if (oldRunIn) console.log('test');
does run multiple times.
This runs multiple times (6 times per use effect) always:
setRunInArrow(oldRunIn => {
console.log('setState is running');
console.log('test');
return newRunInArrow;
});
This does not run multiple times:
setRunInArrow(oldRunIn => {
console.log('setState is running');
return newRunInArrow;
});
Edit 2:
Reproducible example. Click the button. You don't even need to have if (oldState) console.log('Old state');
you can just put in 2nd console.log and it will change the behavior.
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>
</>;
};
Edit 3:
I've reproduced this by putting this code into another nextJS project. I'm guessing it has to do with nextJS.
Edit 4:
It's not NextJS. It's wrapping it in React.StrictMode. Here is a sandbox. Why?
Edit 5:
As the answer pointed out the issue is due to StrictMode intentionally running code twice. It should not run useReducer twice per the docs (this is an issue with "react-hooks/exhaustive-deps" from my other question). Here is a UseReducer demo, try with and without StrictMode. It also runs twice. Seems like it needs to be pure too:
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;
The output of the code below when oldRunIn is undefined
is as expected when the effect is triggered:
Effect is running
setState is running
However, the next time useEffect runs with the state variable runInArrow
defined, which is referred to as oldRunInArrow
in the setState function the output is:
Effect is running
setState is running
setState is running
setState is running
How is it possible that the effect runs only one time but the setState runs 3 times?
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]);
To understand why I'm using a function to set state see this post
Edit:
This is strange. if I remove if (oldRunIn) map.removeOverlays(oldRunIn);
it works as expected. However if I change it to if (oldRunIn) console.log('oldRunIn is defined');
it still runs multiple times. I'm thoroughly confused.
if (oldRunIn) map.addAnnotations([]);
does not run multiple times.
if (oldRunIn) console.log('test');
does run multiple times.
This runs multiple times (6 times per use effect) always:
setRunInArrow(oldRunIn => {
console.log('setState is running');
console.log('test');
return newRunInArrow;
});
This does not run multiple times:
setRunInArrow(oldRunIn => {
console.log('setState is running');
return newRunInArrow;
});
Edit 2:
Reproducible example. Click the button. You don't even need to have if (oldState) console.log('Old state');
you can just put in 2nd console.log and it will change the behavior.
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>
</>;
};
Edit 3:
I've reproduced this by putting this code into another nextJS project. I'm guessing it has to do with nextJS.
Edit 4:
It's not NextJS. It's wrapping it in React.StrictMode. Here is a sandbox. Why?
Edit 5:
As the answer pointed out the issue is due to StrictMode intentionally running code twice. It should not run useReducer twice per the docs (this is an issue with "react-hooks/exhaustive-deps" from my other question). Here is a UseReducer demo, try with and without StrictMode. It also runs twice. Seems like it needs to be pure too:
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;
Share
Improve this question
edited Jun 20, 2020 at 9:12
CommunityBot
11 silver badge
asked Feb 19, 2020 at 16:34
DieselDiesel
5,3537 gold badges53 silver badges84 bronze badges
10
- 1 Can you provide a simple working snippet, as the code shown it's not obvious why this is happening. – Keith Commented Feb 19, 2020 at 16:49
- 1 How to create a Minimal, Reproducible Example – Dennis Vash Commented Feb 19, 2020 at 16:49
- 1 There must be something else going on because I cannot reproduce this with a simple example. – goto Commented Feb 19, 2020 at 17:02
- I added a reproducible example, also I should mention I'm using nextJs – Diesel Commented Feb 19, 2020 at 17:03
- 1 @7iiBob Yes, it's TypeScript. – JLRishe Commented Feb 19, 2020 at 20:03
2 Answers
Reset to default 9As you've figured out, this is happening when you use React strict mode, and it is intentional.
As noted in this article:
It runs code TWICE
Another thing that React Strict Mode does is run certain callbacks/methods twice (in DEV mode ONLY). You read that right! The following callbacks/methods will be run twice in Strict Mode (in DEV mode ONLY):
- Class ponent constructor method
- The render method (includes function ponents)
- setState updater functions (the first argument)
- The static getDerivedStateFromProps lifecycle
- The React.useState state initializer callback function
- The React.useMemo callback
Checkout this codesandbox which logs to the console in hook callbacks and class methods to show you that certain things happen twice.
React does this because it cannot reliably warn you against side-effects you do in those methods. But if those methods are idempotent, then calling them multiple times shouldn't cause any trouble. If they are not idempotent, then you should notice funny things which you should hopefully be able to notice and fix.
Note that useEffect and useLayoutEffect callbacks are not called twice even in dev mode + strict mode because the entire point of those callbacks is to perform side-effects.
Note that I also observed that the reducer you pass to React.useReducer is not called twice in dev mode. I'm not sure why this is because I feel like that could also benefit from this kind of warning.
As seen above, this behavior is included to help you find bugs in your code. Since the updater functions should be idempotent and pure, running them twice instead of once should have no effect on your app's functionality. If it does, then that's a bug in your code.
It sounds like the map.removeOverlays()
method you're calling is an effectful function, so it should not be called within a state updater function. I can see why you've implemented it this way based on the answers to your other question, but I think using chitova263's answer would remedy this issue.
I am running this piece of code and the useEffect() is getting called initially once by default at ponent mounting?
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;