react: Bug: React 18 Strict mode does not simulate unsetting and re-setting DOM refs
React version: 18.1.0
Steps To Reproduce
- Use
<React.StrictMode> - Set a ref to a JSX element in a component
- Create a
useLayoutEffect/useEffectin the component where the returned cleanup function console logs the ref - Save and refresh the app
- You should have access to the ref element in the the
useLayoutEffect/useEffectcleanup function during the simulated unmount.
Link to code example: https://codesandbox.io/s/proud-snow-ox4ngx?file=/src/App.js
The current behavior
Does not unset/re-set refs in simulated unmount. This could lead to unexpected bugs in development like having double-set event listeners, i.e.if( ref.current ){ //add eventlistener to it }and might not match the behavior of actually unmounting the DOM node as described in the docs: https://reactjs.org/docs/strict-mode.html
On the second mount, React will restore the state from the first mount. This feature simulates user behavior such as a user tabbing away from a screen and back, ensuring that code will properly handle state restoration.
The expected behavior
In normal unmounts and mounts refs are unset(before layout effects are cleaned up) and set(before layout effects).
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 40
- Comments: 28
I want to put things a little differently than what has been said here.
By not resetting refs, the React “double render” stress test creates behavior that only exists in development and would never occur in production build and therefore creates a use case that we now have to develop for just for the sake of the “double render” stress test in development. Here’s an example:
In the following code I want this behavior:
This code will have different behavior in development than in production. In development since refs are not reset this component will always try to scroll the first appointment into view and will not try to restore the last scroll position a user had when they were on this page.
Why? In production this effect would only run once if the day never changes. In development this runs twice no matter what.
We have workarounds to make the behavior consistent but again, we have to develop a workaround that would never happen in production just to satisfy the “double render” stress test.
It’s possible React has some future plans to say “hey we are gonna run those damn effects as many times as we please” and if so makes this point moot. Thanks for your thoughts!
Just my two cents here to re-iterate the problem in a different way. In my case, I’m setting up a store instance in a component via a ref hook (simplified version):
then I have a disposer that looks something like this:
And here’s the bit in the component:
In my case, the store is calling an api call that uses cancel tokens. However, in strict mode, because the refs aren’t automatically cleaned up as part of the second rerender, it gets into a weird state where the new component does the whole mounting flow, but the ref link gets lost, causing the component’s second mount to not see the resulting data. As soon as I comment out the
ref.current = null;line above, it works because the ref now can keep the link behind the scenes, therefore only a single store instance is actually used. However, this defeats the point of the strict mode, since now you’ve got components rerendering, but a store instance that is shared between the two mounts.Long story short (and a lot of trial and error later), I landed on this github issue because I had to turn off the strict mode to properly keep my garbage collection flow working. It would be nice to be able to verify the api promise canceling via the strict mode (which is what I would expect), but the refs not getting cleared properly makes this impossible.
On our project we also caught up into that trap. For now we can’t use
StrictModeat all as it breaks all our applications. We haveusePreviousrecommend by react team, which looks like:And we have component that display tiled image. Each tile - is independent instance that use custom hooks to load image, cache it, aborts previous call if was invoked. So it looks like:
What will happen above in StrictMode? Let’s see:
useEffect (2), compare params and callhandleLoaduseEffect (1), and abort request that was called in p1.useEffect (2)and does nothing next.paramsandprevParamsare equal.usePreviouswas not re-created. As result - tile image was not loaded, nothing displayed to the developer.useRefisuseStateunder the hood. Why does React keep state in StrictMode between mounting/umounting? What if I want it only in few components? What if want decide by myself when it should happed to component or even custom hook, but not React?Hi! Any updates for this?
For example, useChangeEffect hook, quite commonly used, I guess. Initial implementation was based on the fact, that after mount ref always has initial value, but now it isn’t true: https://codesandbox.io/s/dry-grass-0uyhx4
Basically, it was like axiom - refs always have initial values after mount. Now the axiom is broken.
Of course, for this particular case you can use cleanup function to set
isMountedref back to false, but if you want to implementuseChangeEffectwith cleanup functions provided by user, you need to use twouseEffects instead of one - one for setting isMount ref (without deps), and the other for actual effect with deps. Something like this:And now we have to use two effects instead of one for implementing the same functionality, just because of artefact of dev mode. That’s kind of strange to me.
@HenrikBechmann I think what you’re getting it is at the core of the issue with this (possible) bug. I think an effective way to check whether a component is mounted would be to use a ref to a jsx/DOM element in the component:
That is the main issue with the functionality mentioned in the issue. If the simulated unmount doesn’t clear the refs like a normal unmount, this pattern can’t be properly used and tested in development StrictMode.
That’s also not to say that this pattern is good or even necessary to use, but in my opinion it should be expected to work this way, however it can’t work this way at the moment due to the (possible) bug.
@huynhit24 That’s intentional behavior in Strict Mode and unrelated to this thread. Please refer to https://reactjs.org/blog/2022/03/29/react-v18.html#new-strict-mode-behaviors.
in React 18 every time I render the component mount, the callback runs twice, I don’t know what to do?