Skip to content

Solid: ssr='data-only' + pendingComponent element causes hydration mismatch / template is not a function #7085

@ljho01

Description

@ljho01

Description

A hydration regression seems to have been introduced for Solid when using ssr: 'data-only' together with a pendingComponent that returns any DOM element.

I reduced this to a very small MRE. The route shape is essentially:

export const Route = createFileRoute('/mre-data-only')({
  ssr: 'data-only',
  loader: async () => {
    await new Promise((r) => setTimeout(r, 1500))
    return 'OK'
  },
  pendingComponent: () => <div />,
  component: () => <div>{Route.useLoaderData()}</div>,
})

On initial load / hard refresh of this route:

  • pendingComponent: () => <div /> fails
  • pendingComponent: () => <div>PENDING</div> fails
  • pendingComponent: () => <p>PENDING</p> fails
  • pendingComponent: () => 'PENDING' works
  • pendingComponent: () => null works

So the minimal failing case appears to be: pendingComponent returns a single DOM element.

Actual behavior

On initial load or refresh, the app hits hydration/runtime errors such as:

  • Hydration Mismatch. Unable to find DOM nodes for hydration key
  • TypeError: template is not a function

and the page can remain stuck in the pending state.

Expected behavior

For ssr: 'data-only', the route should hydrate cleanly and transition from pendingComponent to the loaded route component.

Version boundary

Working:

  • @tanstack/solid-start@1.166.18
  • @tanstack/solid-router@1.167.5

Broken:

  • @tanstack/solid-start@1.167.0
  • @tanstack/solid-router@1.168.0

Still broken on latest tested:

  • @tanstack/solid-start@1.167.15
  • @tanstack/solid-router@1.168.9

Suspected regression

The behavior change appears to line up with the Match.tsx refactor in commit 0545239 (refactor: signal based reactivity).

In the previously working version (@tanstack/solid-router@1.167.5), the outer suspense fallback was effectively disabled for resolvedNoSsr routes on both server and client:

(isServer ?? router.isServer) || resolvedNoSsr
  ? undefined
  : <Dynamic component={resolvePendingComponent()} />

In @tanstack/solid-router@1.168.0, this became:

(isServer ?? router.isServer) && resolvedNoSsr
  ? undefined
  : <Dynamic component={resolvePendingComponent()} />

For ssr: 'data-only', resolvedNoSsr is true, so this changes client behavior:

  • old behavior: outer suspense fallback is also disabled on the client
  • new behavior: outer suspense fallback is enabled on the client, but still disabled on the server

That seems to create the hydration mismatch.

Extra verification

I also monkey-patched the latest installed @tanstack/solid-router locally and changed only that condition back from && to ||.

With that one-line change:

  • hydration succeeded
  • template is not a function disappeared
  • pendingComponent: () => <div /> worked again
  • route completed from pending to OK

Related

This may be related to #6824, but this MRE seems narrower: ssr: 'data-only' + pendingComponent returning any DOM element is enough to trigger it.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions