Update - Not Recommended
AG Grid no longer uses this approach or recommends that you use this Hook either. Instead ensure that your React components correctly implement their own clear down logic so that whether StrictMode is used or not your component works consistently.
Original Post Content
React 18 introduced a breaking change, when in Strict Mode, all components mount and unmount, then mount again.
For React Hooks in React 18, this means a useEffect() with zero dependencies will be executed twice.
Here is a custom hook that can be used instead of useEffect(), with zero dependencies, that will give the old (pre React 18) behaviour back, i.e. it works around the breaking change.
Here is the custom hook useEffectOnce without TypeScript:
export const useEffectOnce = ( effect )=> {
const destroyFunc = useRef();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const [val, setVal] = useState(0);
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffect( ()=> {
// only execute the effect first time around
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}
// this forces one render after the effect is run
setVal(val => val + 1);
return ()=> {
// if the comp didn't render since the useEffect was called,
// we know it's the dummy React cycle
if (!renderAfterCalled.current) { return; }
if (destroyFunc.current) { destroyFunc.current(); }
};
}, []);
};
And here is the hook again with TypeScript:
export const useEffectOnce = (effect: () => void | (() => void)) => {
const destroyFunc = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const [val, setVal] = useState<number>(0);
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffect(() => {
// only execute the effect first time around
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}
// this forces one render after the effect is run
setVal((val) => val + 1);
return () => {
// if the comp didn't render since the useEffect was called,
// we know it's the dummy React cycle
if (!renderAfterCalled.current) {
return;
}
if (destroyFunc.current) {
destroyFunc.current();
}
};
}, []);
};
In your application code, call useEffectOnce with zero dependencies instead of useEffect. Job Done.
// instead of this:
useEffect( ()=> {
console.log('my effect is running');
return () => console.log('my effect is destroying');
}, []);
// do this:
useEffectOnce( ()=> {
console.log('my effect is running');
return () => console.log('my effect is destroying');
});
How it works in a nutshell, I observed in React 18, if the effect runs, and then gets destroyed again before it renders, we know it's a fake setup / destroy cycle. This works regardless of what React version, and regardless of whether Strict Mode is used or not.
Top comments (11)
Using this trick should be avoided and there is probably a better way. By opting in to use this trick you risk your application to break in production at a later date when React releases a feature called "Reusable state" Reusable state will allow for unmounting and remounting of components without losing state. React 18 introduces the double firing of useEffect in dev mode to prepare us for this upcoming change and stress test your components.
If your component is not behaving correctly in dev mode now. It wont behave correctly in production later!
reference - reactjs.org/blog/2022/03/29/react-...
It's important to update our mental models of how useEffect works from React 18 onwards as it is now subtly different. This video I found hugely helpful
youtube.com/watch?v=MXSuOR2yRvQ
Thanks for sharing the video link. It's really helpful.
Probably just a minor copy/paste bug, but there is an extra open parenthesis here that doesn't have a matching close below.
I am also seeing a problem when Strict mode is not enabled or app is build in production mode, the unmount event will not fire.
Here's my code for testing:
Output in production mode when mounting and unmounting the component once.
useEffectOnce exit
was expected to fire here.Thanks, we've updated the code to fix typos and hopefully addressed Jack's point below.
useEffectOnce exit
is not going to fire as far as I can see. This hook works to defeat the hook being called twice, but the cleanup functions will never get called as far as I can see.I honestly don't know how to fix this. I've been trying everything I can think of.
Thanks for pointing this out Jack. Niall updated the code, hopefully it is better.
What is the difference between development and production hook behavior? Could you please explain.
more info here: React 18 — the trickiness of useEffect
Final code like this might be better.
This happens only in development mode not in production mode . So should we change the code to handle behaviour only for the development mode.
No, this can happen in production. It's just that they do it on purpose on dev. But in production, it can happen. They did not activate this on dev just to piss devs off.