Skip to content

fix: preserve data script content during client hydration#6653

Merged
schiller-manuel merged 2 commits intoTanStack:mainfrom
nikuscs:fix-preserve-data-script-content-hydration
Feb 14, 2026
Merged

fix: preserve data script content during client hydration#6653
schiller-manuel merged 2 commits intoTanStack:mainfrom
nikuscs:fix-preserve-data-script-content-hydration

Conversation

@nikuscs
Copy link
Contributor

@nikuscs nikuscs commented Feb 14, 2026

Fixes #6062
Fixes #6627
Closes #6569

Problem

The Script component in Asset.tsx renders all <script> tags with empty content on the client, then recreates them via useEffect / DOM manipulation. This is necesary for executable scripts (they need document.createElement to actually run), but it causes data-only scripts like application/ld+json to flash empty during hydration — and in some cases the content never comes back properly.

Fix

Per the HTML spec, any <script> with a type thats not text/javascript or module is a "data block" — the browser will never execute it. So we can safely render those with their content directly (like <style> tags already do in the same file), skipping the useEffect recreation entirely.

Changes across react-router, solid-router and vue-router:

  • Detect data scripts via the type attribute
  • Early-return in the client-side effect
  • Render data script content directly instead of emptying it

Test cases added

  • application/ld+json renders content on SSR
  • application/ld+json preserves content on client hydration
  • application/json preserves content on client hydration
  • text/javascript (executable) still renders empty on client (unchanged behavior)
  • module scripts still render empty on client (unchanged behavior)

Summary by CodeRabbit

  • New Features

    • Preserve and correctly render inline "data" scripts (e.g., JSON-LD, application/json and similar non-JS types) across React, Solid, and Vue router lifecycles for consistent SSR/CSR behavior while keeping script attributes.
  • Tests

    • Added end-to-end SSR/CSR tests covering data-script rendering, content preservation, executable/module handling, and deduplication to ensure a single DOM instance.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Adds detection and client render path for non-JS "data" scripts (e.g., application/ld+json) across React, Solid, and Vue router packages and introduces React tests. Inline data-script children are rendered into script.innerHTML on the client instead of leaving empty placeholders or duplicating elements.

Changes

Cohort / File(s) Summary
React Router Script
packages/react-router/src/Asset.tsx
Adds dataScript detection (attrs.type is a non-empty string and not text/javascript/module), early-return in the effect for data scripts, and client render of inline string children via dangerouslySetInnerHTML instead of leaving an empty placeholder.
React Router Tests
packages/react-router/tests/Scripts.test.tsx
Adds SSR/CSR test suite for data-script rendering: SSR output, client preservation of data scripts, exec/module handling, deduplication in document.head, and navigation/hydration scenarios.
Solid Router Script
packages/solid-router/src/Asset.tsx
Adds dataScript predicate, short-circuits dynamic injection on mount for data scripts, and renders inline string children into a <script> with innerHTML on the client path.
Vue Router Script
packages/vue-router/src/Asset.tsx
Adds dataScript detection in setup, prevents dynamic injection on mount for data scripts, and renders inline content with innerHTML on client render while preserving attrs and mismatch handling.

Sequence Diagram(s)

sequenceDiagram
  participant SSR as Server (SSR)
  participant Browser as Browser (initial HTML)
  participant Client as Client Runtime
  participant Head as document.head

  SSR->>Browser: Serve HTML with <script type="application/ld+json">content</script>
  Browser->>Client: Load bundle and hydrate
  Client->>Client: Script component evaluates attrs & children
  alt dataScript && children is string
    Client->>Head: Render/ensure <script> with innerHTML (no dynamic injection)
  else src or JS/module script
    Client->>Head: Effect injects or re-inserts executable/module <script>
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • brenelz

Poem

🐰
I nibble code beneath the log,
No empty scripts left in the fog.
LD+JSON snug in its tag,
Hydration tidy — no duplicate drag.
Hooray! Data scripts now stay. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Failed to fetch base branch: From https://github.com/TanStack/router
! [rejected] main -> main (non-fast-forward)
+ 2148b1b...1dde024 main -> origin/main (forced update)
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main fix: preventing data script content from being lost during client-side hydration.
Linked Issues check ✅ Passed All code changes align with linked issues #6062, #6627, and #6569: data scripts are now detected by type and rendered with content on the client instead of rendering empty, preventing loss of content and duplication.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: modifications to Asset.tsx in react-router, solid-router, and vue-router to detect and properly render data scripts, plus comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix-preserve-data-script-content-hydration
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nikuscs nikuscs force-pushed the fix-preserve-data-script-content-hydration branch 2 times, most recently from 1874b8c to 1472165 Compare February 14, 2026 00:16
@nikuscs nikuscs marked this pull request as ready for review February 14, 2026 00:20
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/react-router/src/Asset.tsx`:
- Around line 52-55: The dataScript classification wrongly treats an
empty-string type as a non-JS data script; update the logic around the
dataScript constant in Asset.tsx (the attrs?.type checks) to treat empty string
as 'text/javascript' by normalizing the type first (e.g., let type =
attrs?.type?.trim(); if type === '' set to undefined or 'text/javascript') or by
guarding with a truthy check (e.g., only compare when attrs.type is truthy),
then use that normalized value in the existing comparisons (attrs.type !==
'text/javascript' and !== 'module') so scripts with type="" are treated as
executable JS.

In `@packages/solid-router/src/Asset.tsx`:
- Around line 42-45: The condition computing const dataScript should guard
against an empty-string type like the other implementations; update the
conditional that checks attrs?.type (in the const dataScript declaration) to
also require attrs.type !== '' so dataScript is false for an empty string, while
preserving the existing checks for attrs.type !== 'text/javascript' and
attrs.type !== 'module'.

In `@packages/vue-router/src/Asset.tsx`:
- Around line 57-60: The dataScript boolean currently treats an empty string
type as a data script; update the condition that computes dataScript in
Asset.tsx to also ensure props.attrs?.type !== '' (i.e., add a check against the
empty string alongside the existing checks for 'text/javascript' and 'module')
so that a blank type value is not misclassified; locate the expression that
assigns dataScript and adjust it to include the props.attrs?.type !== '' guard.
🧹 Nitpick comments (1)
packages/vue-router/src/Asset.tsx (1)

57-60: dataScript is not reactive — won't update if props.attrs.type changes after mount.

Since dataScript is a plain const computed once in setup(), it won't react to prop changes. This is likely fine since script types don't change dynamically, but if reactivity is ever needed, wrap it in Vue.computed().

@nikuscs nikuscs force-pushed the fix-preserve-data-script-content-hydration branch from 1472165 to 22cda23 Compare February 14, 2026 00:27
The Script component renders all <script> tags with empty content on
the client side, then uses useEffect to recreate them via DOM
manipulation. While necessary for executable scripts (they need
document.createElement to trigger execution), this causes data-only
scripts like application/ld+json to briefly flash empty during
hydration.

Per the HTML spec, any script with a type that isn't a JavaScript MIME
type or "module" is a "data block" that the browser never executes.
These can safely render their content directly via innerHTML on both
server and client, just like <style> tags already do in the same file.

Changes across react-router, solid-router, and vue-router:
- Detect non-executable script types via the type attribute
- Early-return in the client-side effect for data scripts
- Render data script content directly instead of emptying it
@nikuscs nikuscs force-pushed the fix-preserve-data-script-content-hydration branch from 22cda23 to 58d8547 Compare February 14, 2026 00:31
@nx-cloud
Copy link

nx-cloud bot commented Feb 14, 2026

View your CI Pipeline Execution ↗ for commit b7f68af

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 10m 46s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 28s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-14 01:03:38 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 14, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6653

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@6653

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6653

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@6653

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@6653

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@6653

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@6653

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6653

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6653

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6653

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@6653

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@6653

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@6653

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@6653

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@6653

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@6653

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@6653

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@6653

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@6653

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@6653

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@6653

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@6653

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6653

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6653

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6653

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6653

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6653

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6653

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6653

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@6653

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6653

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6653

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6653

@tanstack/vue-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router@6653

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-devtools@6653

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-ssr-query@6653

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6653

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6653

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6653

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6653

commit: b7f68af

@schiller-manuel schiller-manuel merged commit fb139e8 into TanStack:main Feb 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants