Skip to content

SSR-enabled beforeLoad on root with ssr: false causes "This Suspense boundary received an update before it finished hydrating." #8180

Description

@fionamatthews-wk

Which project does this relate to?

Router

Describe the bug

When using SSR with tanstack start and router, with a route that has a beforeLoad, a router change-of-state triggers a useSyncExternalStore update / callback. Since useSyncExternalStore is always synchronous, it ignores the startTransition scope of the call stack, fires off a state change immediately, and invalidates the Suspense boundary of the SSR hydration.

The result is this error:

react-dom.development.js:20741 Uncaught Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.
    at updateDehydratedSuspenseComponent (react-dom.development.js:20741:48)
    at updateSuspenseComponent (react-dom.development.js:20401:16)
    at beginWork (react-dom.development.js:21663:14)
    at beginWork$1 (react-dom.development.js:27465:14)
    at performUnitOfWork (react-dom.development.js:26596:12)
    at workLoopSync (react-dom.development.js:26505:5)
    at renderRootSync (react-dom.development.js:26473:7)
    at performSyncWorkOnRoot (react-dom.development.js:26124:20)
    at flushSyncCallbacks (react-dom.development.js:12042:22)
    at flushPassiveEffectsImpl (react-dom.development.js:27099:3)

(Note the fix supplied in the error text is wrong. useSyncExternalStore is never compatible with suspense. See here).

Complete minimal reproducer

https://github.com/fionamatthews-wk/tanstack-router-beforeload-ssr-hydration-error

Steps to Reproduce the Bug

  1. pnpm install
  2. pnpm dev
  3. Visit http://localhost:3000

Note hydration error in console.

Expected behavior

No hydration error. SSR correctly loads and hydrates the server copy without falling back to a full client render.

Screenshots or Videos

No response

Platform

  • Router / Start Version: 1.170.32 / 1.168.49
  • OS: MacOS
  • Browser: Chrome
  • Browser Version: 151.0.7922.173
  • Bundler: vite
  • Bundler Version: 8.2.2

Additional context

It seems likely that #7805 introduced this.

#8055 seems like an attempt to fix this, but our specific case is still receiving a suspense boundary and erroring as of 1.170.32.

I suspect the issue is more to do with calling beforeLoad triggers a store change that a component re-renders for, rather than a question of whether or not the root should be wrapped in suspense.

I placed a breakpoint inside of most useSyncExternalStore subscribers during the initial hydration, and Match.tsx was the first component I found whose useSyncExternalStore (via useStore) is triggering a react update event. Previously, Match.tsx had a filter on its store change. Now, it does not. I suspect that this change, or others like it, are what is causing a synchronous state change callback during suspense that causes react to bail.

I do wonder if ssr: false on a root with SSR enabled is in conflict. However, our current intent is to use SSR for injection of nonces, with investigation into enabling SSR on specific routes postponed until later. Changing ssr to true in the reproduction does get rid of the error, but our intent is not to SSR the root at this time and still have beforeLoad implement our authentication.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions