-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: React 18 Strict mode does not simulate unsetting and re-setting DOM refs #24670
Comments
Thanks, will check with the team. |
I confirm. In my case, this causes a nasty bug. Everything related refs that worked in version 17 just stopped working in version 18 without any error messages. I definitely can't use React.StrictMode right now. Moreover, during the first installation using |
According to this post:
It makes sense to keep the user-managed values around as they are part of the component state that will be restored, however refs to DOM elements maybe should not be set? |
I may be missing something but this statement sounds massively overblown. There are lots of libraries using refs that work fine with React 18. While the issue discussed here might be legit (we're discussing that), can you share what kind of code you have that doesn't work? Specific examples are most helpful. |
Also something to consider is that the current effect cleanup order is different between simulated(bubble) and standard(capture) unmounts. |
in React 18 every time I render the component mount, the callback runs twice, I don't know what to do? |
@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. |
Hi! Any updates for this?
For example, useChangeEffect hook, quite commonly used, I guess. Basically, it was like axiom - refs always have initial values after mount. Of course, for this particular case you can use cleanup function to set
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. |
I think you're confusing the issue in this thread ( |
I'm not sure, but it looked like these cases have the same reason. Anyway, thank you for the answer! I'll create a separated issue, then. |
@gaearon I use
Then I check isMountedRef.current before changing state in a hook component. The use case is an infiniteScroller, where async unmounting is common. This business of a "test" unmount in strict mode is screwing that up. It means if someone downloads my library during dev mode with strict mode, my library doesn't work. So, I'm not happy! Any suggestions of an alternative way of checking if a component is still mounted? As a general comment, this looks like one of those cases in which a kind of patronizing attitude to developers with unexpected behaviours causes unwanted side effects. Just saying... |
Further to this, your documentation about this says that you 'restore state' after your faux unmount, so I used a setIsMounted(false) call from useState in a useEffect cleanup function (returned from non-dependent useEffect), hoping that the state would be returned to true, as your documentation states. But, no such luck. State is not restored after the faux unmount, according to my tests. Maybe the solution here is to finally give us a native isComponentMounted() function. Just a thought... Any advice from anyone would be appreciated. This is a nasty issue for me. |
Ok, so my workaround is this. I find that with React.StrictMode useEffect with no dependencies actually runs twice (in spite of your documentation promising it only runs once). So my revised code:
I have my fingers crossed that this happens fast enough to avoid some of my processes being wrongly missed while @Gaeron you've been quoted as saying that this approach is "lazy"; that the correct thing to do is to "cancellation". But here's what I'm missing - if the reason to cancel a process is because your component has been unmounted, and there's NO WAY TO TELL IF YOUR COMPONENT IS UNMOUNTED, then how the heck are you supposed to know when to cancel something?? There's 'abortController.abort()', but that also is run in a useEffect cleanup routine, so with React.StrictMode it is run incorrectly!! Like, seriously, what are you talking about? |
@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. |
Are there any updates to this issue? Using refs to track a component's first mount isn't possible in strict mode at the moment because of this bug. Unless with a workaround cleanup function to reset it. function App() {
let firstMount = useRef(true)
useEffect(()=>{
if (firstMount.current) {
firstMount.current = false;
console.log('This should log')
return () => firstMount.current = true // I'd rather not leave workarounds that will eventually expire in my code
}
console.log('This shouldnt log')
},[])
} Having to choose between the benefits of strict mode and having callbacks that'll need to be removed in the future throughout my app is definitely not easy. I hope the team can fix this soon. |
I'm having the same issue but with In const [pluginServer] = useState(
() => new PluginServer({ url, name, document, ...options })
);
useEffect(() => {
return () => {
pluginServer.dispose();
}
}, [pluginServer]); |
We just ran into this as well. In our case we're attempting to set React state with the result of an async operation, so in order to prevent state updates on an unmounted component we use a ref to track whether the component is mounted or not (as seems to be a common pattern). With this change to StrictMode Contrived example: const [result, setResult] = useState(null);
const mounted = useRef(true);
useEffect(() => {
return () => mounted.current = false;
}, []);
useEffect(() => {
asyncAPI.then((asyncResponse) => {
if (mounted.current) { // Always false with new StrictMode behavior
setResult(asyncResponse) // State never becomes anything other than null
}
});
}, []); The solution presented in this comment appears to fix this. But I'm wondering if addressing this is indeed our responsibility or if this should be addressed in React itself? Or is there a better way to determine if it's safe to update state? |
On our project we also caught up into that trap. For now we can't use function usePrevious<T>(value: T): T | null {
const ref = useRef<T | null>(null);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
} 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: function useTileLoad<T>(params: T, abortableCallback: AbortableCallback<T>) {
const abortRef = useRef<() => void>();
const [tile, setTile] = useState<TileImage | null>(null);
const prevParams = usePrevious(params); // Store previous params
// Callback to load tile image or re-request new one if params was changed before request completed
const handleLoad = useCallback(async (params: T) => {
if (abortRef.current) {
abortRef.current(); // If abortable is exist - abort it, as we will request new one below
}
let aborted = false; // Temp variable to avoid state update, when request was canceled due to new request or when component unmounts
const abortable = abortableCallback(params); // Call abortable request that returns abortion callback
// Store current abortion request to ref
abortRef.current = () => {
aborted = true;
abortable.abort();
};
try {
const response = await abortable();
if (!aborted) {
setTile(response);
} else {
throw new Error('Request was canceled');
}
} catch (e) {
console.log('Request was failed or aborted', e);
}
}, [abortableCallback]);
// (1) Abort current request Promise wrapper to avoid unhandled state update
useEffect(() => {
return () => {
abortRef.current?.();
};
}, []);
// (2) Compare next params with previous params and request new tile in prams are different
useEffect(() => {
if (!isEqual(params, prevParams)) {
handleLoad(params);
}
}, [props, prevProps, handleLoad])
} What will happen above in StrictMode? Let's see:
|
@tsarapke unrelated. The issue relates specifically to refs set to DOM elements. Looks like maybe yours can be solved by: // ...
const prevParams = useRef<T>(null);
// ...
if(!aborted){
prevParams.current = params;
setTile(response)
}
// ... But hard to say exactly when your effect (2) will run because you didn't respect exhaustive-deps and your dependencies are totally wrong. I recommend first making your effects work properly with no deps array, then add it back with all requirements for optimization. |
@JMartinCollins, As about issue - I think it still related. The root cause is the same. Many people at the beginning of topic provides same issue-cases. But can you tell why does |
@tsarapke in a way it kind of is related. I think the approach that you should be taking with this hook is to pass it a DOM element ref. Then, you'll only run your abort cleanup if that ref is Also the incorrect deps may just be a typo? |
Yes, there is a typo. Must be I really understand the proposal that React team wants to provide in a future by simulation that thing with |
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:
const Schedule = ({ day }: { day: string }) => {
const scrollRef = useRef(null)
const hasMounted = useRef(false)
// I think useLayoutEffect is need because we want to get
// the scroll position of an element before unmounting
useLayoutEffect(() => {
if(!hasMounted.current) {
hasMounted.current = true
const scrollPosition = getLastScrollPositionLocalStorage(day)
if(scrollPosition && scrollRef.current) {
scrollRef.current.scrollTo(scrollPositon)
return () => {
setLastScrollPositionLocalStorage(day, getScrollPosition(scrollRef.current))
}
}
}
if(scrollRef.current) {
const firstAppointmentScrollPosition = getFirstAppointmentScrollPosition()
scrollRef.current.scrollTo(firstAppointmentScrollPosition)
}
return () => {
setLastScrollPositionLocalStorage(day, getScrollPosition(scrollRef.current))
}
}, [day])
return <ScheduleView ref={scrollRef} />
} 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 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. |
👍 @runfaj Just found this ticket by running into the same issue for the same use case |
Also just ran into this, in our case we were using a ref to store a reference to an instance of our SDK client which was being created in a Provider-style component. The However, the ref to that client was shared across the strict mode unmount-remount process, so two "distinct" component instances were actually sharing a reference and interfering with each other. The client instance of the remounted component was already closed by the time it rendered.. Here's the PR to work around the bug: |
Any update? It takes a lot of time to investigate and work around issues related to this buggy behavior. |
See also this thread: reactjs/react.dev#6123 |
React version: 18.1.0
Steps To Reproduce
<React.StrictMode>
useLayoutEffect
/useEffect
in the component where the returned cleanup function console logs the refuseLayoutEffect
/useEffect
cleanup 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.htmlThe expected behavior
In normal unmounts and mounts refs are unset(before layout effects are cleaned up) and set(before layout effects).
The text was updated successfully, but these errors were encountered: