-
Notifications
You must be signed in to change notification settings - Fork 349
fix(R3F version): Resolve compatibility with React StrictMode #1286
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
base: master
Are you sure you want to change the base?
fix(R3F version): Resolve compatibility with React StrictMode #1286
Conversation
Thank! I think one issue here is that plugins were never intended to be unregistered and then registered again. Once disposed a new plugin should be created if it needs to be added again (this is one of the reasons this flag is on the class in the first place). Right now the current life cycle events are intended to guarantee the following operation order:
I'm wondering if we move the "registeration" and "member assignment" step into the same initial "useLayoutEffect" call to avoid this issue. Something like so: useLayoutEffect( () => {
const plugin = new PluginConstructor( ...args );
assignDeepOptions( plugin, options );
tiles.registerPlugin( plugin );
return () => tiles.unregisterPlugin( plugin );
}, [ PluginConstructor, tiles, useObjectDep( args ) ] ); // "options" is explicitly ignored so we don't recreate the plugin when the options are changed.
useDeepOptions( instance, options ); What do you think? I'm also wondering if we should switch to use React 19 in the dev dependencies for the sake of testing. |
Probably best to use the latest react and r3f version, but this issue can be tested by adding as a first child of for now. I think some dependencies still ask for @react-three/fiber v8 so I did not change it.
Then you solution is probably best but it comes with a breaking change for the r3f version of the package I think. Same for tiles, to support StrictMode I had to extract it from useMemo and place it in a useEffect. The current commit works with the examples. But maybe there is a better way. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes!
Until now you were guaranteed to have the plugin instance available in the context from the first render while now it will only be available after useLayoutEffect runs.
Same for tiles, to support StrictMode I had to extract it from useMemo and place it in a useEffect.
It's possible to support StrictMode while still using useMemo but the .dispose method was breaking the state of the tiles and I did not figure out why.
🤦 These life cycle functions are so convoluted. From pmndrs/react-three-fiber#3549 (comment), I was under the impression that useLayoutEffect
would always run initially - though I know useMemo
probably runs synchronously, instead. If we use useLayoutEffect
rather than useEffect
do we still have these issues? The reason I switched to useLayoutEffect
in the first place was because it seemed to be the case that useEffect
could be delayed where layout effect was not.
It doesn't look like there are any other "effect"-style hooks we could try using to adjust the life cycle, is that right?
I think some dependencies still ask for @react-three/fiber v8 so I did not change it.
Thanks I'll take a look at this another time.
…nder while keeping compatibility with StrictMode
I'll definitely agree to that 😅 I'll try to explain the differences. React hooks like useEffect and useLayoutEffect both run after the body of the function ( React component ) has been executed while useMemo runs immediately and its result is available immediately within the body of the function during render. The difference between useLayoutEffect and useEffect is indeed that useLayoutEffect runs immediatly, but still after the component did render. In the most recent commit I reverted most of my changes to handle the logic of registering / unregistering directly in the body of the component instead of relying on effects. This fix the issue with plugins. For the main Tiles instance and the support of StrictMode, we needed a way to call .dispose() only when a component fully unmounts. It's not very elegant but it works. The alternative and maybe more "r3f way", would be to use r3f extend() and let r3f manage the dispose. As a user, it fits my needs and fix the issues with StrictMode without causing any breaking change. |
…in-logic # Conflicts: # example/r3f/atmosphere.jsx
…/3DTilesRendererJS into fix-unregister-plugin-logic
@AlaricBaraou - this should be fixed now. Take a look and let me know what you think. The changes boil down to not relying on I've also fixed an issue with CanvasDOMOverlay resulting in console errors:
edit: if it all looks good I'll probably get rid of the "dispose" example, as well, unless you think it's needed |
…rer instance is provided
Awesome, today I learned with your changes that StrictMode only runs the effect setup and cleanup functions twice during the component's initial mount. I thought they ran twice each time, which make your solution viable indeed.
I understand and it's weird to have an example specific to R3F but in this instance it's the example that allowed me to find an issue in the lifecycle management of Previous to 617ec11 when changing a tile instance, the I also thought about adding
I suspect one potential bug in `TilesPlugin``thought. A potential solution could be to replace this safeguard
With
This would cause any children of the plugin to unmount but it guarantees that the children will only run with a valid Tiles + Plugin pair. I tested both with and without and both work fine with the current examples. |
I see this is working by swapping tile set urls. Is it possible we can add a test for something like this? Otherwise we can keep it for now but maybe it makes more sense to integrate this into another demo. I just want to keep the number of demos to maintain to a minimum.
This looks like a good change - we should just add a comment describing why "tiles" is not included in the dependencies array.
Thanks for the catch - this is required. It looks like I forgot to readd the dispose call after migrating from the previous r3f group dispose approach.
Interesting. I'm not exactly sure how we would be supposed to work around this in react, which is annoying. For some of hte current plugin children this should be okay but why don't we add your check. I don't love accessing |
Actually my suggestion was wrong, because of this check the component can be left in a state where it will not render the children even after being registered, because we don't update the component state when registering the plugin. I think the best thing to do is to register the plugin when it is created in the same useEffect. If the options are meant to be applied only once on creation before registering, then it's just about moving the content of the useDeepOptions effect in a function like we did earlier in this PR. If it's meant to be editable, with the current ability to restore the previous state when an option change, it's more tricky. |
In react in strict mode for example you can have
<TilesPlugin />
useLayoutEffect
running twice and registering, unregistering then registering again the plugin causing it to throw the error 'TilesRendererBase: A plugin can only be registered to a single tile set'.https://react.dev/reference/react/StrictMode#enabling-strict-mode-for-entire-app
As to why this wasn't noticed in earlier version of r3f, the StrictMode wasn't inherited within the r3f component tree prior to v9.
See https://r3f.docs.pmnd.rs/tutorials/v9-migration-guide#strictmode