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

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