Skip to content
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

useShape can miss shape updates between the shape is created and subscribed to leading to loading state forever #2336

Closed
hedblad opened this issue Feb 13, 2025 · 4 comments · Fixed by #2408
Assignees

Comments

@hedblad
Copy link

hedblad commented Feb 13, 2025

When the Electric SQL server connection is established quicker than the React subscription is set up, the shape returned by useShape will be stuck in loading until the shape is modified.

It is tricky to provide a proper reliable reproduction case. We run Postgres and Electric in Docker Compose and can only reproduce this in Chrome under some quite specific circumstances, since this is a race condition and React is involved. So I will reason about the code instead.

Here are lines 163-180 of packages/react-hooks/src/react-hooks.tsx:

  const useShapeData = React.useMemo(() => {
    let latestShapeData = parseShapeData(shape)
    const getSnapshot = () => latestShapeData
    const subscribe = (onStoreChange: () => void) =>
      shapeSubscribe(shape, () => {
        latestShapeData = parseShapeData(shape)
        onStoreChange()
      })

    return () => {
      return useSyncExternalStoreWithSelector(
        subscribe,
        getSnapshot,
        getSnapshot,
        selector
      )
    }
  }, [shape, selector])

If shape loads between the initial parseShapeData and subscribe is called by React, this change will not be picked up by React and shape will be stuck in the loading state forever (or at least until the data changes).

Or to put it in different terms: things might change between setting up the shape and subscribing to it, which means we won't pick up the shape data or loading state. Since we don't have control over when react actually calls the function subscribing to the stream, it would make sense to always update the state when the subscription is created to ensure consistency.

Changing the implementation to the following fixes the issue:

const useShapeData = useMemo(() => {
    let latestShapeData = parseShapeData(shape)
    const getSnapshot = () => latestShapeData
    const subscribe = (onStoreChange: () => void) => {
      const updateShapeData = () => {
        latestShapeData = parseShapeData(shape)
        onStoreChange()
      }
      // Trigger an update immediately since we might have missed changes 
      // between setting up the ShapeStream and subscribing to it
      updateShapeData()
      return shapeSubscribe(shape, updateShapeData)
    }
    return () => {
      return useSyncExternalStoreWithSelector(
        subscribe,
        getSnapshot,
        getSnapshot,
        selector
      )
    }
  }, [shape, selector]);

A similar issue in TanStack/query is described in this issue: TanStack/query#5443 and fixed with this PR: TanStack/query#5474

We can work around this by updating the selector in a useEffect to trigger an update but that feels like a bit of a hack.

@KyleAMathews
Copy link
Contributor

KyleAMathews commented Feb 14, 2025

Huh, very interesting! Somehow we've never encountered this but the scenario you describe sounds plausible and the tanstack bug sounds exactly the same.

Could you put together a PR w/ your fix and a similar test as Tanstack added?

Thanks for the detailed bug report!

@balegas balegas added bug and removed bug labels Feb 17, 2025
@balegas balegas added this to the Next major release milestone Feb 17, 2025
@hedblad
Copy link
Author

hedblad commented Feb 28, 2025

Thanks for the quick response!

Sorry for not getting back sooner, I've been trying to create a reproduction case for a test without success. I'm not sure what it is in our particular case that causes React to delay the subscription here. I suspect it's due to heavy rendering that takes precedence over subscribe since subscribe is pushed to the fiber as a passive effect.

Do you have any idea on how I could trigger this behavior in a test?

@msfstef
Copy link
Contributor

msfstef commented Mar 6, 2025

Very hard to add a test case with the current structure of the code - we'd have to rewire somethings and introduce more mocks, whereas currently it relies heavily on more integrated testing. I've introduced the suggested fix along with some other changes that could have potentially made it lose some updates, so I think this should be fixed.

I've managed to reproduce the issue with some ad-hoc code changes to the source to simulate the situation, and these changes fix that issue. If it persists after this change do not hesitate to reopen this or another issue, and we'll spend more time on actually wiring things up for a test reproduction.

@hedblad
Copy link
Author

hedblad commented Mar 6, 2025

Amazing! Thanks a lot being so responsive, quick and understanding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants