Skip to content

Comments

fix: script hydration#6681

Merged
schiller-manuel merged 2 commits intomainfrom
react-script-hydration
Feb 16, 2026
Merged

fix: script hydration#6681
schiller-manuel merged 2 commits intomainfrom
react-script-hydration

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Added support for user-included scripts in head and body with async options (scripts now execute and set global flags visible in the browser).
  • Bug Fixes

    • Improved hydration-aware script rendering to prevent duplication, preserve attributes (e.g., async, crossorigin), and ensure correct execution order across SSR and client navigation.
  • Tests

    • Added end-to-end tests for script execution and updated unit tests to reflect client-side script injection behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Adds hydration-aware handling for user scripts: server renders script markup, client preserves markup pre-hydration and imperatively injects scripts post-hydration; includes E2E fixtures and updated tests to validate script execution and avoid hydration mismatches.

Changes

Cohort / File(s) Summary
E2E public scripts
e2e/react-start/basic/public/async-user-script.js, e2e/react-start/basic/public/before-scripts-async-script.js, e2e/react-start/basic/public/before-scripts-script.js, e2e/react-start/basic/public/head-async-script.js, e2e/react-start/basic/public/head-script.js, e2e/react-start/basic/public/user-script.js
Six small client scripts added that log load messages and set window flags to signal execution (head/before/after positions and async variants).
Root layout
e2e/react-start/basic/src/routes/__root.tsx
Inserted script tag references in head, before Scripts, and after Scripts (mix of async and non-async) to exercise load order and hydration behavior.
E2E tests — root scripts
e2e/react-start/basic/tests/root-scripts.spec.ts
New test suite verifying script execution flags and absence of hydration errors across SSR hydration and client navigation.
E2E tests — duplication
e2e/react-start/basic/tests/script-duplication.spec.ts
Updated assertions to reflect React 19/script hoisting: moved from exact counts to "not duplicated" checks, waits for script execution flags, and removed some hydration-warning assertions.
Core Script component
packages/react-router/src/Asset.tsx
Refactored Script handling to use hydration state (useHydrated): server renders script markup; client renders identical markup pre-hydration to avoid mismatches and injects scripts imperatively after hydration; added dev warning when both src and children are provided.
Unit tests for scripts
packages/react-router/tests/Scripts.test.tsx
Updated tests to assert scripts are injected into document.head on client (not rendered in React container), preserve attributes, and added setup/teardown to clear head/body between tests.

Sequence Diagram(s)

sequenceDiagram
    participant Server
    participant Client
    participant Hydration
    participant ScriptInjection as Script (useEffect)
    participant DOM

    rect rgba(200, 150, 255, 0.5)
    Note over Server,DOM: Server renders script markup
    Server->>DOM: Render <script> (src or inline)
    end

    rect rgba(150, 200, 255, 0.5)
    Note over Client,Hydration: Client pre-hydration rendering
    Client->>Hydration: check hydrated?
    Hydration-->>Client: No
    Client->>DOM: Render matching <script> node (prevents mismatch)
    Hydration->>Client: Yes (hydration complete)
    Client->>ScriptInjection: useEffect runs
    ScriptInjection->>DOM: Imperatively inject script into head (avoid React tree)
    ScriptInjection->>Client: Component returns null post-hydration
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • birkskyum

Poem

🐰 I hopped through head and body too,
I set flag bits and logged "loaded" too,
Hydration safe, no duplicate mess,
Tests hop by to check success,
A little rabbit cheers this fix — woo-hoo! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 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 ❌ Merge conflicts detected (109 files):

⚔️ e2e/react-router/escaped-special-strings/src/routeTree.gen.ts (content)
⚔️ e2e/react-router/escaped-special-strings/src/routes/__root.tsx (content)
⚔️ e2e/react-router/escaped-special-strings/tests/escaped-routes.spec.ts (content)
⚔️ e2e/react-start/basic/src/routes/__root.tsx (content)
⚔️ e2e/react-start/basic/tests/script-duplication.spec.ts (content)
⚔️ examples/react/authenticated-routes-firebase/package.json (content)
⚔️ examples/react/authenticated-routes/package.json (content)
⚔️ examples/react/basic-file-based/package.json (content)
⚔️ examples/react/basic-react-query-file-based/package.json (content)
⚔️ examples/react/basic-ssr-file-based/package.json (content)
⚔️ examples/react/basic-ssr-streaming-file-based/package.json (content)
⚔️ examples/react/basic-virtual-file-based/package.json (content)
⚔️ examples/react/basic-virtual-inside-file-based/package.json (content)
⚔️ examples/react/i18n-paraglide/package.json (content)
⚔️ examples/react/kitchen-sink-file-based/package.json (content)
⚔️ examples/react/kitchen-sink-react-query-file-based/package.json (content)
⚔️ examples/react/large-file-based/package.json (content)
⚔️ examples/react/quickstart-esbuild-file-based/package.json (content)
⚔️ examples/react/quickstart-file-based/package.json (content)
⚔️ examples/react/quickstart-rspack-file-based/package.json (content)
⚔️ examples/react/quickstart-webpack-file-based/package.json (content)
⚔️ examples/react/router-monorepo-react-query/package.json (content)
⚔️ examples/react/router-monorepo-react-query/packages/router/package.json (content)
⚔️ examples/react/router-monorepo-simple-lazy/package.json (content)
⚔️ examples/react/router-monorepo-simple-lazy/packages/router/package.json (content)
⚔️ examples/react/router-monorepo-simple/package.json (content)
⚔️ examples/react/router-monorepo-simple/packages/router/package.json (content)
⚔️ examples/react/search-validator-adapters/package.json (content)
⚔️ examples/react/start-bare/package.json (content)
⚔️ examples/react/start-basic-auth/package.json (content)
⚔️ examples/react/start-basic-authjs/package.json (content)
⚔️ examples/react/start-basic-cloudflare/package.json (content)
⚔️ examples/react/start-basic-react-query/package.json (content)
⚔️ examples/react/start-basic-rsc/package.json (content)
⚔️ examples/react/start-basic-static/package.json (content)
⚔️ examples/react/start-basic/package.json (content)
⚔️ examples/react/start-bun/package.json (content)
⚔️ examples/react/start-clerk-basic/package.json (content)
⚔️ examples/react/start-convex-trellaux/package.json (content)
⚔️ examples/react/start-counter/package.json (content)
⚔️ examples/react/start-i18n-paraglide/package.json (content)
⚔️ examples/react/start-large/package.json (content)
⚔️ examples/react/start-material-ui/package.json (content)
⚔️ examples/react/start-streaming-data-from-server-functions/package.json (content)
⚔️ examples/react/start-supabase-basic/package.json (content)
⚔️ examples/react/start-tailwind-v4/package.json (content)
⚔️ examples/react/start-trellaux/package.json (content)
⚔️ examples/react/start-workos/package.json (content)
⚔️ examples/react/view-transitions/package.json (content)
⚔️ examples/react/with-trpc-react-query/package.json (content)
⚔️ examples/react/with-trpc/package.json (content)
⚔️ examples/solid/authenticated-routes-firebase/package.json (content)
⚔️ examples/solid/authenticated-routes/package.json (content)
⚔️ examples/solid/basic-file-based/package.json (content)
⚔️ examples/solid/basic-solid-query-file-based/package.json (content)
⚔️ examples/solid/basic-solid-query/package.json (content)
⚔️ examples/solid/basic-ssr-file-based/package.json (content)
⚔️ examples/solid/basic-ssr-streaming-file-based/package.json (content)
⚔️ examples/solid/basic-virtual-file-based/package.json (content)
⚔️ examples/solid/basic-virtual-inside-file-based/package.json (content)
⚔️ examples/solid/i18n-paraglide/package.json (content)
⚔️ examples/solid/kitchen-sink-file-based/package.json (content)
⚔️ examples/solid/kitchen-sink-solid-query-file-based/package.json (content)
⚔️ examples/solid/large-file-based/package.json (content)
⚔️ examples/solid/quickstart-esbuild-file-based/package.json (content)
⚔️ examples/solid/quickstart-file-based/package.json (content)
⚔️ examples/solid/quickstart-rspack-file-based/package.json (content)
⚔️ examples/solid/quickstart-webpack-file-based/package.json (content)
⚔️ examples/solid/router-monorepo-simple-lazy/package.json (content)
⚔️ examples/solid/router-monorepo-simple-lazy/packages/router/package.json (content)
⚔️ examples/solid/router-monorepo-simple/package.json (content)
⚔️ examples/solid/router-monorepo-simple/packages/router/package.json (content)
⚔️ examples/solid/router-monorepo-solid-query/package.json (content)
⚔️ examples/solid/router-monorepo-solid-query/packages/router/package.json (content)
⚔️ examples/solid/search-validator-adapters/package.json (content)
⚔️ examples/solid/start-basic-auth/package.json (content)
⚔️ examples/solid/start-basic-authjs/package.json (content)
⚔️ examples/solid/start-basic-cloudflare/package.json (content)
⚔️ examples/solid/start-basic-netlify/package.json (content)
⚔️ examples/solid/start-basic-nitro/package.json (content)
⚔️ examples/solid/start-basic-solid-query/package.json (content)
⚔️ examples/solid/start-basic-static/package.json (content)
⚔️ examples/solid/start-basic/package.json (content)
⚔️ examples/solid/start-bun/package.json (content)
⚔️ examples/solid/start-convex-better-auth/package.json (content)
⚔️ examples/solid/start-counter/package.json (content)
⚔️ examples/solid/start-i18n-paraglide/package.json (content)
⚔️ examples/solid/start-large/package.json (content)
⚔️ examples/solid/start-streaming-data-from-server-functions/package.json (content)
⚔️ examples/solid/start-supabase-basic/package.json (content)
⚔️ examples/solid/start-tailwind-v4/package.json (content)
⚔️ examples/solid/view-transitions/package.json (content)
⚔️ examples/solid/with-trpc/package.json (content)
⚔️ examples/vue/basic-file-based-jsx/package.json (content)
⚔️ examples/vue/basic-file-based-sfc/package.json (content)
⚔️ packages/react-router/src/Asset.tsx (content)
⚔️ packages/react-router/tests/Scripts.test.tsx (content)
⚔️ packages/react-start/package.json (content)
⚔️ packages/router-cli/package.json (content)
⚔️ packages/router-generator/package.json (content)
⚔️ packages/router-generator/src/filesystem/physical/getRouteNodes.ts (content)
⚔️ packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts (content)
⚔️ packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts (content)
⚔️ packages/router-plugin/package.json (content)
⚔️ packages/router-vite-plugin/package.json (content)
⚔️ packages/solid-start/package.json (content)
⚔️ packages/start-plugin-core/package.json (content)
⚔️ packages/start-static-server-functions/package.json (content)
⚔️ packages/vue-start/package.json (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: script hydration' directly describes the main change, which is addressing hydration-aware handling for scripts in the Asset.tsx component and related test updates.

✏️ 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
  • Commit unit tests in branch react-script-hydration
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch react-script-hydration
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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

@nx-cloud
Copy link

nx-cloud bot commented Feb 16, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit cf06bd9

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 12m 27s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 37s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-16 18:31:35 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 16, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: cf06bd9

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: 1

🤖 Fix all issues with AI agents
In `@e2e/react-start/basic/tests/script-duplication.spec.ts`:
- Around line 35-45: The current page.evaluate uses a substring selector
'script[src*="script.js"]' which incorrectly matches root layout scripts; update
the selector used in the scriptCount evaluation to a more specific attribute
match (for example use an ends-with selector like 'script[src$="/script.js"]' or
an exact match 'script[src="script.js"]') so only the route-specific /script.js
is counted; keep the surrounding logic (the waitForFunction on (window as
any).SCRIPT_1 and the expect on scriptCount) but replace the selector in the
page.evaluate call that defines scriptCount.
🧹 Nitpick comments (2)
packages/react-router/tests/Scripts.test.tsx (1)

206-208: Minor: Use block body to avoid implicit return in forEach callback.

The arrow function (s) => s.remove() implicitly returns the result of remove() (which is undefined). While harmless, using a block body makes intent clearer.

Suggested fix
     // Clear head and any leftover body scripts between tests.
     document.head.innerHTML = ''
-    document.querySelectorAll('body script').forEach((s) => s.remove())
+    document.querySelectorAll('body script').forEach((s) => {
+      s.remove()
+    })
e2e/react-start/basic/tests/root-scripts.spec.ts (1)

44-60: Consider clarifying the test description.

The test verifies that root layout scripts (which executed on the initial /posts load) remain active after client-side navigation to Home. The current name "should execute on client-side navigation" could be misread as testing that scripts execute during navigation rather than persisting across navigation.

A clearer name might be: "root layout scripts should remain executed after client-side navigation".

This is a minor documentation concern and doesn't affect test correctness.

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.

🧹 Nitpick comments (1)
e2e/react-start/basic/tests/script-duplication.spec.ts (1)

35-47: The SSR test no longer validates script non-duplication.

The change from an exact count assertion to toBeGreaterThanOrEqual(1) means this test now only verifies that the script exists and executes, but no longer catches script duplication. The client-side navigation tests (lines 59-72) still assert toBe(1) for the same purpose.

If React 19 hoisting makes exact counts unreliable for SSR, consider asserting toBeLessThanOrEqual(1) (like the inline scripts test at line 130) to at least ensure no duplication, while still relying on the execution check for "at least once":

Suggested alternative
-    expect(scriptCount).toBeGreaterThanOrEqual(1)
+    // Script executed (checked via waitForFunction above), now verify no duplication
+    expect(scriptCount).toBeLessThanOrEqual(1)

Alternatively, if scripts can legitimately appear multiple times in the DOM due to React 19 hoisting, the test name "should not create duplicate scripts" may need updating to reflect the actual invariant being tested.

@schiller-manuel schiller-manuel merged commit 27a563e into main Feb 16, 2026
5 of 6 checks passed
@schiller-manuel schiller-manuel deleted the react-script-hydration branch February 16, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant