Skip to content

fix: dont use script tag for OnRendered#7054

Merged
schiller-manuel merged 3 commits intomainfrom
no-script-onrendered
Mar 26, 2026
Merged

fix: dont use script tag for OnRendered#7054
schiller-manuel merged 3 commits intomainfrom
no-script-onrendered

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Mar 26, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Removed extra server-side markup used for internal render tracking while preserving scroll restoration and onRendered timing.
  • Tests
    • Updated SSR test expectations to match the new server rendering output.
  • Documentation
    • Clarified post-commit/onRendered documentation to reflect the new behavior.
  • Chores
    • Added a patch release entry for this change.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcf6cea3-cfaf-4225-b8da-0d0c81d38698

📥 Commits

Reviewing files that changed from the base of the PR and between 83aac8a and 2021b7f.

📒 Files selected for processing (2)
  • .changeset/calm-suns-thank.md
  • packages/react-router/src/Match.tsx
✅ Files skipped from review due to trivial changes (1)
  • .changeset/calm-suns-thank.md

📝 Walkthrough

Walkthrough

Refactors OnRendered from a DOM script-sentinel to a hook-based emission via useLayoutEffect, adds a resetKey prop to control emission timing, adds a server-side guard, updates tests to remove an empty SSR script, and updates a comment in the Solid router variant. (43 words)

Changes

Cohort / File(s) Summary
Release Configuration
\.changeset/calm-suns-thank.md
Adds a patch changeset noting removal of an extra SSR sentinel tag while keeping client-side render timing behavior.
React Router match & render logic
packages/react-router/src/Match.tsx
Replaces script-tag + ref approach with OnRendered({ resetKey }) that uses useLayoutEffect, adds server-side early return, emits router.onRendered when href changes, and depends on [__TSR_key, resetKey, router].
Tests
packages/react-router/tests/Scripts.test.tsx
Updates SSR test expectation to remove an empty <script></script> placeholder; expected output now only includes actual script tags.
Docs / Comments (Solid)
packages/solid-router/src/Match.tsx
Updates OnRendered documentation comment to reflect the hook-based approach and removes references to dummy DOM/script-tag tracking.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped around the SSR bed, keeping watch with tiny tag,
Now I crouch in layout hooks — no more empty script flag.
I nudge the router, drum a beat, emit when hrefs change,
A tidy hop, a little squeak, the render flow rearranged. 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately summarizes the main change: removing script tag usage for OnRendered functionality across the codebase.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch no-script-onrendered

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

@nx-cloud
Copy link

nx-cloud bot commented Mar 26, 2026

View your CI Pipeline Execution ↗ for commit 2021b7f

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

☁️ Nx Cloud last updated this comment at 2026-03-26 23:54:22 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Mar 26, 2026

🚀 Changeset Version Preview

1 package(s) bumped directly, 3 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.168.5 → 1.168.6 Changeset
@tanstack/react-start 1.167.10 → 1.167.11 Dependent
@tanstack/react-start-client 1.166.20 → 1.166.21 Dependent
@tanstack/react-start-server 1.166.20 → 1.166.21 Dependent

@github-actions
Copy link
Contributor

github-actions bot commented Mar 26, 2026

Bundle Size Benchmarks

  • Commit: 0e0a2817c300
  • Measured at: 2026-03-26T23:43:20.434Z
  • Baseline source: history:70b222513720
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.53 KiB 0 B (0.00%) 275.87 KiB 76.06 KiB ████▇▇▃▃▁▁▁
react-router.full 90.84 KiB +19 B (+0.02%) 287.06 KiB 78.97 KiB ▇███▇▇▂▂▁▁▁▁
solid-router.minimal 35.56 KiB 0 B (0.00%) 107.23 KiB 31.90 KiB ████▄▄▄▄▁▁▁
solid-router.full 40.04 KiB 0 B (0.00%) 120.77 KiB 35.93 KiB ▅███▂▂▂▂▁▁▁
vue-router.minimal 53.42 KiB 0 B (0.00%) 153.17 KiB 47.88 KiB ██████▁▁▁▁▁
vue-router.full 58.28 KiB 0 B (0.00%) 168.63 KiB 52.20 KiB ▆█████▂▂▁▁▁
react-start.minimal 102.05 KiB +24 B (+0.02%) 324.05 KiB 88.30 KiB ▆███▆▆▂▂▁▁▁▁
react-start.full 105.43 KiB +18 B (+0.02%) 334.40 KiB 91.13 KiB ▇███▆▆▂▂▁▁▁▁
solid-start.minimal 49.65 KiB 0 B (0.00%) 153.42 KiB 43.76 KiB ████▄▄▄▄▁▁▁
solid-start.full 55.16 KiB 0 B (0.00%) 169.66 KiB 48.56 KiB ▄███▃▃▃▃▁▁▁

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 26, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7054

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7054

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7054

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7054

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7054

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7054

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7054

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7054

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7054

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7054

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7054

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7054

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7054

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7054

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7054

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7054

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7054

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7054

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7054

commit: 2021b7f

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-router/src/Match.tsx`:
- Around line 221-243: The server check and href-based dedupe are wrong: move
the isServer check inside the existing useLayoutEffect so the hook is called
unconditionally, and replace the href comparison with a dedupe based on the
location key (router.latestLocation.state.__TSR_key) so new history entries with
the same href still emit; specifically, keep the
useLayoutEffect([...router.latestLocation.state.__TSR_key, resetKey, router])
but change prevHrefRef to a prevKeyRef (or compare prevHrefRef.current against
router.latestLocation.state.__TSR_key) and only return early from the effect
when isServer is true, otherwise call router.emit({ type: 'onRendered',
...getLocationChangeInfo(router.stores.location.state,
router.stores.resolvedLocation.state) }) and update the prevKeyRef to the
current __TSR_key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d91d736-fff0-4a86-b3e3-fdf744ec664e

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0a281 and 83aac8a.

📒 Files selected for processing (4)
  • .changeset/calm-suns-thank.md
  • packages/react-router/src/Match.tsx
  • packages/react-router/tests/Scripts.test.tsx
  • packages/solid-router/src/Match.tsx

Comment on lines +221 to +243
if (isServer ?? router.isServer) {
return null
}

return (
<script
key={router.latestLocation.state.__TSR_key}
suppressHydrationWarning
ref={(el) => {
if (
el &&
(prevLocationRef.current === undefined ||
prevLocationRef.current.href !== router.latestLocation.href)
) {
router.emit({
type: 'onRendered',
...getLocationChangeInfo(
router.stores.location.state,
router.stores.resolvedLocation.state,
),
})
prevLocationRef.current = router.latestLocation
}
}}
/>
)
const prevHrefRef = React.useRef<string | undefined>(undefined)

useLayoutEffect(() => {
const currentHref = router.latestLocation.href

if (
prevHrefRef.current === undefined ||
prevHrefRef.current !== currentHref
) {
router.emit({
type: 'onRendered',
...getLocationChangeInfo(
router.stores.location.state,
router.stores.resolvedLocation.state,
),
})
prevHrefRef.current = currentHref
}
}, [router.latestLocation.state.__TSR_key, resetKey, router])
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that OnRendered currently has both:
# 1) conditional return before hook call
# 2) dedupe by href while effect depends on __TSR_key

rg -n -C4 'function OnRendered|if \(isServer \?\? router\.isServer\)|useLayoutEffect\(|prevHrefRef|latestLocation\.href|__TSR_key' packages/react-router/src/Match.tsx

Repository: TanStack/router

Length of output: 2601


🏁 Script executed:

# Search for __TSR_key definition and usage to understand state changes
rg -n '__TSR_key' packages/react-router/src/ -A 2 -B 2 | head -60

Repository: TanStack/router

Length of output: 383


🏁 Script executed:

# Check the router's latestLocation and state structure
rg -n 'latestLocation' packages/react-router/src/Match.tsx -B 2 -A 2

Repository: TanStack/router

Length of output: 314


🏁 Script executed:

# Look for tests of the OnRendered component
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts | xargs rg -l 'OnRendered' 2>/dev/null | head -5

Repository: TanStack/router

Length of output: 188


🏁 Script executed:

# Verify the hooks rule concern by checking if there are any linter configs or comments
rg -n 'react-hooks' packages/react-router/ -t ts -t tsx | head -20

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Search for __TSR_key definition in router core
rg -n '__TSR_key\s*[=:]' packages/ -t ts -t tsx -B 3 -A 3 | head -80

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Look for tests related to onRendered
rg -n 'onRendered' packages/react-router/tests/ -B 2 -A 5 | head -100

Repository: TanStack/router

Length of output: 1307


🏁 Script executed:

# Check router state definition and latestLocation type
rg -n 'latestLocation.*=' packages/react-router/src/ -t ts -A 2 | head -40

Repository: TanStack/router

Length of output: 307


🏁 Script executed:

# Search for comments or context around the OnRendered function
sed -n '200,250p' packages/react-router/src/Match.tsx

Repository: TanStack/router

Length of output: 1497


🏁 Script executed:

# Search for __TSR_key definition with simpler pattern
rg '__TSR_key' packages/router-core/src/ -A 3 -B 1 | head -100

Repository: TanStack/router

Length of output: 2213


🏁 Script executed:

# Search in all files for __TSR_key definition
rg '__TSR_key\s*=' packages/ -A 2 -B 2 | head -80

Repository: TanStack/router

Length of output: 519


🏁 Script executed:

# Check the location state type definition
rg -n 'state:.*__TSR_key|__TSR_key.*:' packages/ -B 3 -A 1 | head -60

Repository: TanStack/router

Length of output: 2710


🏁 Script executed:

# Look for any related issues or comments about href vs __TSR_key
rg -n 'href.*__TSR_key|__TSR_key.*href' packages/ -B 2 -A 2

Repository: TanStack/router

Length of output: 445


Move server check inside the effect and dedupe by __TSR_key instead of href.

The onRendered event fails to emit when navigating to the same URL as a new history entry (different __TSR_key). The dependency array correctly tracks __TSR_key, but the dedupe logic compares href, suppressing the emit when URLs match despite the history entry being new. Additionally, the conditional return before useLayoutEffect violates React Hooks rules (hooks must be called unconditionally on every render).

Suggested fix
 function OnRendered({ resetKey }: { resetKey: number }) {
   const router = useRouter()
-
-  if (isServer ?? router.isServer) {
-    return null
-  }
-
-  const prevHrefRef = React.useRef<string | undefined>(undefined)
+  const prevRenderKeyRef = React.useRef<string | undefined>(undefined)

   useLayoutEffect(() => {
-    const currentHref = router.latestLocation.href
-
-    if (
-      prevHrefRef.current === undefined ||
-      prevHrefRef.current !== currentHref
-    ) {
+    if (isServer ?? router.isServer) return
+    const currentRenderKey = router.latestLocation.state.__TSR_key
+    if (prevRenderKeyRef.current !== currentRenderKey) {
       router.emit({
         type: 'onRendered',
         ...getLocationChangeInfo(
           router.stores.location.state,
           router.stores.resolvedLocation.state,
         ),
       })
-      prevHrefRef.current = currentHref
+      prevRenderKeyRef.current = currentRenderKey
     }
   }, [router.latestLocation.state.__TSR_key, resetKey, router])

   return null
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-router/src/Match.tsx` around lines 221 - 243, The server check
and href-based dedupe are wrong: move the isServer check inside the existing
useLayoutEffect so the hook is called unconditionally, and replace the href
comparison with a dedupe based on the location key
(router.latestLocation.state.__TSR_key) so new history entries with the same
href still emit; specifically, keep the
useLayoutEffect([...router.latestLocation.state.__TSR_key, resetKey, router])
but change prevHrefRef to a prevKeyRef (or compare prevHrefRef.current against
router.latestLocation.state.__TSR_key) and only return early from the effect
when isServer is true, otherwise call router.emit({ type: 'onRendered',
...getLocationChangeInfo(router.stores.location.state,
router.stores.resolvedLocation.state) }) and update the prevKeyRef to the
current __TSR_key.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what was the behavior before when it comes to same url?

this does not violate rules of hooks as the condition is constant

Copy link
Contributor

Choose a reason for hiding this comment

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

@schiller-manuel, you're right on both points — my review comment was mistaken.

Same-URL behavior before: The old <script> approach also used an href guard — even though the key={__TSR_key} change caused React to remount the element and fire the ref callback, the prevLocationRef.current.href !== router.latestLocation.href check would still suppress the onRendered emit when the href was the same. So the new code preserves the exact same behavior: same-URL navigation (even with a different __TSR_key) does not fire onRendered again, same as before.

Rules of Hooks: You're correct — isServer is a module-level constant and router.isServer is set once at router construction and never mutates. The expression isServer ?? router.isServer evaluates to the same value on every render, so the early return path never changes between renders and does not violate the Rules of Hooks in practice.

I apologize for the noise.

(´・ω・`)


🧠 Learnings used
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Learnt from: nlynzaad
Repo: TanStack/router PR: 6389
File: e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx:11-24
Timestamp: 2026-01-18T17:36:24.388Z
Learning: In TanStack Router e2e malformed test routes (e.g., `e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx`), intentional use of `href` prop instead of `to` prop on Link components may be used for testing purposes to validate specific encoding/decoding behaviors or edge cases, even though the standard `tanstack/solid-router` Link API uses the `to` prop.

Learnt from: Sheraff
Repo: TanStack/router PR: 6171
File: packages/router-core/src/new-process-route-tree.ts:898-898
Timestamp: 2025-12-21T12:52:35.231Z
Learning: In `packages/router-core/src/new-process-route-tree.ts`, the matching logic intentionally allows paths without trailing slashes to match index routes with trailing slashes (e.g., `/a` can match `/a/` route), but not vice-versa (e.g., `/a/` cannot match `/a` layout route). This is implemented via the condition `!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX` and is a deliberate design decision to provide better UX by being permissive with missing trailing slashes.

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 26, 2026

Merging this PR will improve performance by 5.04%

⚡ 1 improved benchmark
✅ 5 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
client-side navigation loop (react) 59.3 ms 56.4 ms +5.04%

Comparing no-script-onrendered (2021b7f) with main (70b2225)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (0e0a281) during the generation of this report, so 70b2225 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@schiller-manuel schiller-manuel merged commit 5ca661c into main Mar 26, 2026
15 of 16 checks passed
@schiller-manuel schiller-manuel deleted the no-script-onrendered branch March 26, 2026 23:54
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