react-three-fiber icon indicating copy to clipboard operation
react-three-fiber copied to clipboard

createPortal()'s state merging uses stale data

Open notrabs opened this issue 1 month ago • 1 comments

Hi there,

when using createPortal(children, scene, { camera }) with a custom state override (e.g. for the camera), the state gets reverted back to its initial value, when the parent-store updates. This also happens for any other state override you provide there.

Here is a sandbox with a minimal reproduction. There the camera within the portal should not change, when the parent store is updated.

image

This bug is only visible, when the createPortal component is not also immeditaly re-rendered by the store update. In that case, the re-render overwrites the state again with the correct values.

image


The root-cause seems to lie in this effect:

React.useEffect(() => {
  // Subscribe to previous root-state and copy changes over to the mirrored portal-state
  const unsub = previousRoot.subscribe((prev) => usePortalStore.setState((state) => inject(prev, state)))
  return () => {
    unsub()
    usePortalStore.destroy()
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

It is missing a dependency on the inject function, which means that it always uses the initial state data to do the state merge.

If I understand it right, I think the hook needs to be split in two:

React.useEffect(() => {
  // Subscribe to previous root-state and copy changes over to the mirrored portal-state
  const unsub = previousRoot.subscribe((prev) => usePortalStore.setState((state) => inject(prev, state)))
  return () => {
    unsub()
  }
}, [inject])

React.useEffect(() => {
  return () => {
    usePortalStore.destroy()
  }
}, [])

notrabs avatar May 07 '24 08:05 notrabs