When using react Strict Mode useEffect
is always invoked at least twice, i.e.
useEffect(() => {
console.log('Hello, World!');
}, []);
The above code will print "Hello, World!" "Hello, World!".
This breaks how I have traditionally implemented side-effects, e.g. log out upon visiting /log-out
page:
export const LogOutPage = () => {
const { logout } = useAuth();
useEffect(() => {
logout({
onCompleted: () => {
console.log('logged out');
},
variables: {},
});
}, []);
return null;
};
useEffect
will be invoked twice and so is onCompleted
, causing "logged out" to be logged twice.
I thought this could be solved by using state to prevent useEffect
from being called the second time...
export const LogOutPage = () => {
const [loggedOut, setLoggedOut] = useState(false);
const { logout } = useAuth();
useEffect(() => {
if (loggedOut) {
return;
}
console.log('loggedOut: ', loggedOut);
setLoggedOut(true);
logout({
onCompleted: () => {
console.log('logged out');
},
variables: {},
});
}, [loggedOut]);
return null;
};
However, because the initial value of loggedOut
is false
, it still executes that code path twice, i.e. it will wring "loggedOut: false" "loggedOut: false"
It would appear that the only way to prevent this is to trigger the desired side-effect by a change in state, i.e.
export const LogOutPage = () => {
const [startedLogOut, setStartedLogOut] = useState(false);
const [loggedOut, setLoggedOut] = useState(false);
const { logout } = useAuth();
useEffect(() => {
// This will be invoked twice.
// However, the operation is idempotent.
setStartedLogOut(true);
}, []);
useEffect(() => {
if (!startedLogOut) {
return;
}
setLoggedOut(true);
if (loggedOut) {
return;
}
logout({
onCompleted: () => {
console.log('logged out');
},
variables: {},
});
}, [startedLogOut, loggedOut]);
return null;
};
This indeed works, however, it feels convoluted, and it is not an pattern I've observed in the wild. Asking this question to confirm that this is how I should be implementing side-effects when using Strict Mode.
CodePudding user response:
Those effects which should be run only once can be handled using useRef
.
export default const LogOutPage = () => {
const [loggedOut, setLoggedOut] = useRef(false);
const { logout } = useAuth();
useEffect(() => {
if (loggedOut.current) {
return;
}
console.log('loggedOut: ', loggedOut.current);
loggedOut.current = true;
logout({
onCompleted: () => {
console.log('logged out');
},
variables: {},
});
}, []);
return null;
};
More reading.
CodePudding user response:
import { type EffectCallback, useEffect, useState, useRef } from 'react';
/**
* @see https://stackoverflow.com/a/72319081/368691
*/
export const useMount = (effect: EffectCallback) => {
const ref = useRef<ReturnType<EffectCallback>>(undefined);
const [mounted, setMounted] = useState<boolean>(false);
const [invoked, setInvoked] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
}, [setMounted]);
useEffect(() => {
if (!mounted) {
return () => {};
}
setInvoked(true);
if (invoked) {
return ref.current;
}
ref.current = effect();
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setInvoked, invoked, mounted]);
};
This implementation:
- correctly invokes the effect on the first mount
- correctly invokes clean
- works in production and Strict Mode