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
- pnpm install
- pnpm dev
- 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.
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
useSyncExternalStoreupdate / callback. SinceuseSyncExternalStoreis always synchronous, it ignores thestartTransitionscope of the call stack, fires off a state change immediately, and invalidates the Suspense boundary of the SSR hydration.The result is this error:
(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
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
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.