TanStack / TanStack/router

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

Open
#8,180 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the supplied reproducer: run pnpm install, pnpm dev, and visit http://localhost:3000 to observe the hydration error. Then inspect packages/react-router/src/Match.tsx, especially its useStore/useSyncExternalStore behavior during beforeLoad and initial hydration, along with the linked earlier version and referenced issues. Done means the SSR-enabled root with ssr: false hydrates without the Suspense error or a full client-render fallback.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.