Skip to content

fix: Fix React Fast Refresh state preservation for auto code-split ro…#7000

Merged
schiller-manuel merged 6 commits intomainfrom
fix-react-hmr
Mar 21, 2026
Merged

fix: Fix React Fast Refresh state preservation for auto code-split ro…#7000
schiller-manuel merged 6 commits intomainfrom
fix-react-hmr

Conversation

@schiller-manuel
Copy link
Contributor

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

…ute components during HMR updates

Summary by CodeRabbit

  • Bug Fixes

    • React Fast Refresh now preserves state for automatically code-split route components during HMR updates.
  • New Features

    • Dev-server warmup utilities to improve test startup reliability.
    • HMR-stable handling for code-split route components and an HMR-capable example workspace/configuration.
  • Tests

    • New end-to-end and unit tests validating HMR behavior, split-component caching, and identifier uniqueness.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 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: dad92436-4a55-4cda-8b03-9f4662f0d89b

📥 Commits

Reviewing files that changed from the base of the PR and between 6f0bd55 and e462090.

📒 Files selected for processing (2)
  • .prettierignore
  • packages/router-plugin/tests/add-hmr/test-files/react/string-literal-keys.tsx
✅ Files skipped from review due to trivial changes (1)
  • .prettierignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-plugin/tests/add-hmr/test-files/react/string-literal-keys.tsx

📝 Walkthrough

Walkthrough

Adds stable HMR caching for automatically code-split route components, extracts shared Playwright dev-server warmup utilities, and scaffolds an HMR-focused E2E React app with tests that validate state preservation across hot updates.

Changes

Cohort / File(s) Summary
Changeset & HMR app config
\.changeset/fair-llamas-tease.md, e2e/react-start/hmr/.gitignore, e2e/react-start/hmr/package.json, e2e/react-start/hmr/tsconfig.json, e2e/react-start/hmr/vite.config.ts, e2e/react-start/hmr/playwright.config.ts
Adds a patch changeset and scaffolds an HMR-focused E2E React workspace with TypeScript, Vite, and Playwright config.
Shared E2E utilities
e2e/e2e-utils/src/devServerWarmup.ts, e2e/e2e-utils/src/index.ts
New exports waitForServer and preOptimizeDevServer for polling dev-server readiness and doing browser warmup with optional ready selector and warmup callback.
E2E setup migrations
e2e/.../tests/setup/global.setup.ts
e2e/react-start/css-modules/tests/setup/global.setup.ts, e2e/react-start/dev-ssr-styles/tests/setup/global.setup.ts, e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts, e2e/solid-start/css-modules/tests/setup/global.setup.ts, e2e/vue-start/css-modules/tests/setup/global.setup.ts
Replaced local server-warmup and polling implementations with imports from @tanstack/router-e2e-utils; updated calls to use { baseURL, readyTestId, warmup } and moved warmup navigations into callbacks.
HMR E2E app sources
e2e/react-start/hmr/src/router.tsx, e2e/react-start/hmr/src/routeTree.gen.ts, e2e/react-start/hmr/src/routes/__root.tsx, e2e/react-start/hmr/src/routes/index.tsx
Adds router factory, generated route tree, root layout, and index route component (client state + hydration marker) used by the HMR test app.
HMR E2E tests & lifecycle
e2e/react-start/hmr/tests/app.spec.ts, e2e/react-start/hmr/tests/setup/global.setup.ts, e2e/react-start/hmr/tests/setup/global.teardown.ts
New Playwright test asserting HMR state preservation for a code-split route component and setup/teardown that use shared warmup utilities.
Router plugin types & utils
packages/router-plugin/src/core/code-splitter/types.ts, packages/router-plugin/src/core/utils.ts
Adds SplitStrategy/SplitNodeMeta types and updates getUniqueProgramIdentifier to reserve program-scope identifiers and avoid collisions across insertions.
Plugin extension points & compiler integration
packages/router-plugin/src/core/code-splitter/plugins.ts, packages/router-plugin/src/core/code-splitter/compilers.ts, packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts
Adds onSplitRouteProperty plugin hook and context; compiler delegates split-route property emission to plugins; React target includes stable-HMR plugin when HMR is enabled; robust key-name resolution for object properties.
React stable HMR plugin
packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts
New plugin injecting deterministic import.meta.hot.data caching and restore logic for split-route components, emitting stable component bindings persisted across HMR updates.
Router plugin tests & snapshots
packages/router-plugin/tests/*, packages/router-plugin/tests/add-hmr/snapshots/react/*, packages/router-plugin/tests/add-hmr/test-files/react/*
Updated tests to use unique identifiers, added collision test for unique id generator, and new/updated snapshots and fixtures illustrating HMR caching and multi-split-component behavior.
Misc tooling
.prettierignore
Added ignore entry for packages/router-plugin/tests/**/test-files/**.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Dev Server
    participant Compiler as Router Plugin Compiler
    participant Browser as Browser (Playwright Page)
    participant HMR as import.meta.hot
    participant Router as Router (window.__TSR_ROUTER__)

    Dev->>Compiler: Build -> produce split importers
    Browser->>Dev: Initial navigation / load
    Browser->>Router: Render route -> lazy split component
    Note right of Dev: Developer edits route file
    Dev->>HMR: HMR update triggered
    HMR->>HMR: Read import.meta.hot.data["tsr-split-component:<name>"]
    alt cached component present
        HMR->>Browser: Reuse cached TSRSplitComponent (preserve instance/state)
    else
        HMR->>Compiler: Re-evaluate lazyRouteComponent importer -> new component
    end
    HMR->>HMR: Persist resolved component into import.meta.hot.data
    HMR->>Router: Invoke hot-accept handler with new Route
    Router->>Router: Replace route entries, clear caches, update segment tree
    Router->>Browser: Re-render route (state preserved via cached component)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled code and warmed the dev page bright,
Cached split components safe in hot-swap light,
HMR hopped in, but state held tight,
Routes kept their stories through the swap at night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% 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 PR title accurately describes the primary change: implementing React Fast Refresh state preservation for code-split route components during HMR updates, which is the core focus of all code changes.

✏️ 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 fix-react-hmr

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

@nx-cloud
Copy link

nx-cloud bot commented Mar 20, 2026

View your CI Pipeline Execution ↗ for commit e462090

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

☁️ Nx Cloud last updated this comment at 2026-03-21 00:18:40 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

🚀 Changeset Version Preview

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

🟩 Patch bumps

Package Version Reason
@tanstack/router-plugin 1.167.1 → 1.167.2 Changeset
@tanstack/react-start 1.167.1 → 1.167.2 Dependent
@tanstack/router-vite-plugin 1.166.16 → 1.166.17 Dependent
@tanstack/solid-start 1.167.1 → 1.167.2 Dependent
@tanstack/start-plugin-core 1.167.4 → 1.167.5 Dependent
@tanstack/vue-start 1.167.1 → 1.167.2 Dependent

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 20, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: e462090

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

Bundle Size Benchmarks

  • Commit: 21e39bde0832
  • Measured at: 2026-03-21T00:18:32.899Z
  • Baseline source: history:91cc62899b75
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 88.56 KiB 0 B (0.00%) 279.34 KiB 76.84 KiB ▁▁▁▁▁▁▁▁███
react-router.full 91.73 KiB 0 B (0.00%) 290.07 KiB 79.48 KiB ▁▁▁▁▁▁▁▁███
solid-router.minimal 36.13 KiB 0 B (0.00%) 108.98 KiB 32.41 KiB ████████▁▁▁
solid-router.full 40.47 KiB 0 B (0.00%) 122.08 KiB 36.26 KiB ████████▁▁▁
vue-router.minimal 54.11 KiB 0 B (0.00%) 155.27 KiB 48.48 KiB ▁▁▁▁▁▁▁▁███
vue-router.full 58.89 KiB 0 B (0.00%) 170.42 KiB 52.63 KiB ▁▁▁▁▁▁▁▁███
react-start.minimal 103.00 KiB 0 B (0.00%) 327.41 KiB 89.02 KiB ▁▁▁▁▁▁▁▁███
react-start.full 106.37 KiB 0 B (0.00%) 337.72 KiB 91.83 KiB ▁▁▁▁▁▁▁▁███
solid-start.minimal 50.32 KiB 0 B (0.00%) 155.47 KiB 44.32 KiB ████████▁▁▁
solid-start.full 55.72 KiB 0 B (0.00%) 171.27 KiB 48.90 KiB ████████▁▁▁

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

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

🧹 Nitpick comments (4)
e2e/e2e-utils/src/devServerWarmup.ts (1)

31-35: Use a type-only import for the Page type.

ESLint flags the inline import() type annotation. Extract the type import at the top of the file for consistency with @typescript-eslint/consistent-type-imports.

♻️ Proposed fix

Add at the top of the file:

import type { Page } from '@playwright/test'

Then update the function signature:

 export async function preOptimizeDevServer(opts: {
   baseURL: string
   readyTestId?: string
-  warmup?: (page: import('@playwright/test').Page) => Promise<void>
+  warmup?: (page: Page) => Promise<void>
 }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/e2e-utils/src/devServerWarmup.ts` around lines 31 - 35, The inline import
type in the preOptimizeDevServer parameter should be replaced with a top-level
type-only import to satisfy consistent-type-imports; add a top-level `import
type { Page } from '@playwright/test'` and change the function signature's
warmup parameter to use `Page` (i.e., warmup?: (page: Page) => Promise<void>),
updating references in preOptimizeDevServer accordingly.
packages/router-plugin/src/core/code-splitter/compilers.ts (1)

858-869: Pass an immutable splitNodeMeta into compiler plugins.

splitNodeMeta is read straight from the module-level SPLIT_NODES_CONFIG map and then handed to plugins. An accidental mutation in a plugin would leak into later properties and later files because that object instance is shared. Passing a shallow clone here would make the new extension point safer.

♻️ Possible hardening
-                          const pluginPropValue = plugin.onSplitRouteProperty?.(
-                            {
-                              programPath,
-                              callExpressionPath: path,
-                              insertionPath,
-                              routeOptions,
-                              prop,
-                              splitNodeMeta,
-                              lazyRouteComponentIdent:
-                                LAZY_ROUTE_COMPONENT_IDENT,
-                            },
-                          )
+                          const pluginPropValue = plugin.onSplitRouteProperty?.(
+                            {
+                              programPath,
+                              callExpressionPath: path,
+                              insertionPath,
+                              routeOptions,
+                              prop,
+                              splitNodeMeta: { ...splitNodeMeta },
+                              lazyRouteComponentIdent:
+                                LAZY_ROUTE_COMPONENT_IDENT,
+                            },
+                          )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-plugin/src/core/code-splitter/compilers.ts` around lines 858
- 869, The code passes the raw splitNodeMeta (sourced from the module-level
SPLIT_NODES_CONFIG) into compiler plugins, risking accidental mutation across
files; instead, create and pass a shallow clone (e.g., via Object.assign({},
splitNodeMeta) or {...splitNodeMeta}) when calling each plugin in the loop that
invokes opts.compilerPlugins and their onSplitRouteProperty callback so plugins
receive an immutable copy; ensure the change is applied where splitNodeMeta is
supplied to onSplitRouteProperty in compilers.ts so the original
SPLIT_NODES_CONFIG entry is not mutated.
e2e/react-start/hmr/tests/app.spec.ts (1)

13-20: Make the HMR file edit fail when the token is ambiguous.

source.replace(from, to) only rewrites the first match. If baseline or updated appears elsewhere in the route file later, this test can silently mutate the wrong spot and send you chasing a fake HMR failure.

🛠️ Proposed hardening
 async function replaceRouteText(from: string, to: string) {
   const source = await readFile(routeFile, 'utf8')
+  const firstIndex = source.indexOf(from)
+  const lastIndex = source.lastIndexOf(from)
 
-  if (!source.includes(from)) {
+  if (firstIndex === -1) {
     throw new Error(`Expected route file to include ${JSON.stringify(from)}`)
   }
+  if (firstIndex !== lastIndex) {
+    throw new Error(`Expected a single match for ${JSON.stringify(from)}`)
+  }
 
   await writeFile(routeFile, source.replace(from, to))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/hmr/tests/app.spec.ts` around lines 13 - 20, In
replaceRouteText, the current source.replace(from, to) only changes the first
occurrence and can silently edit the wrong spot; instead read the routeFile
source, count occurrences of the from token and if count === 0 throw the
existing error, if count > 1 throw a new error indicating the token is
ambiguous, and only when count === 1 perform the replacement and writeFile;
reference replaceRouteText, routeFile, and the from/to parameters when making
this change.
packages/router-plugin/tests/add-hmr/snapshots/react/[email protected] (1)

23-53: Keep the emitted HMR helper typed under strict TS.

oldRoute, newRoute, route, and node are implicit anys here. If this snapshot is meant to reflect the generated *.tsx output, deriving these from typeof Route / typeof router.processedTree.segmentTree would keep the HMR path type-safe.
As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

🔎 One low-friction way to keep the types
-      (function handleRouteUpdate(oldRoute, newRoute) {
+      (function handleRouteUpdate(
+        oldRoute: typeof Route,
+        newRoute: typeof Route,
+      ) {
@@
-        function walkReplaceSegmentTree(route, node) {
+        function walkReplaceSegmentTree(
+          route: typeof Route,
+          node: typeof router.processedTree.segmentTree,
+        ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/router-plugin/tests/add-hmr/snapshots/react/multi-component`@true.tsx
around lines 23 - 53, The helper emits untyped parameters (oldRoute, newRoute,
route, node) causing implicit any under strict TS; annotate handleRouteUpdate
and its inner walkReplaceSegmentTree with explicit types derived from your
runtime shapes (e.g., use the Route type for oldRoute/newRoute/route and the
SegmentTree node type for node — you can import or declare a RouteLike and
SegmentNodeLike interface matching router.processedTree.segmentTree), and add
those types to the function signatures (handleRouteUpdate(oldRoute: RouteLike,
newRoute: RouteLike) and walkReplaceSegmentTree(route: RouteLike, node:
SegmentNodeLike) and any variables like filter as needed) so the snapshot emits
strictly-typed TS code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/react-start/hmr/package.json`:
- Around line 13-20: The package.json uses the non-canonical internal workspace
specifier `workspace:^` for internal packages; update the dependency entries for
"@tanstack/react-router", "@tanstack/react-start" and
"@tanstack/router-e2e-utils" (and any other internal deps using `workspace:^`)
to use the canonical `workspace:*` specifier to match repo conventions and avoid
manifest churn.

In `@e2e/react-start/hmr/tests/app.spec.ts`:
- Around line 1-4: ESLint import/order is violated: move the built-in Node
"node:" imports (readFile, writeFile from 'node:fs/promises' and path from
'node:path') above the external package imports (expect from '@playwright/test'
and test from '@tanstack/router-e2e-utils'); reorder the import statements so
the node: imports come first to satisfy import/order (locate the imports at the
top of app.spec.ts where readFile, writeFile, path, expect, and test are
declared).

In
`@packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts`:
- Around line 37-43: The early-return only allows identifier keys, skipping
quoted keys like 'component' or 'errorComponent'; update the check around
ctx.prop.key and REACT_STABLE_HMR_COMPONENT_IDENTS so it accepts both Identifier
and StringLiteral keys by extracting the key name (use ctx.prop.key.name for
Identifier and ctx.prop.key.value for StringLiteral) and testing that string
against REACT_STABLE_HMR_COMPONENT_IDENTS instead of relying solely on
t.isIdentifier; keep the existing t import checks but branch to handle string
literals before returning.

---

Nitpick comments:
In `@e2e/e2e-utils/src/devServerWarmup.ts`:
- Around line 31-35: The inline import type in the preOptimizeDevServer
parameter should be replaced with a top-level type-only import to satisfy
consistent-type-imports; add a top-level `import type { Page } from
'@playwright/test'` and change the function signature's warmup parameter to use
`Page` (i.e., warmup?: (page: Page) => Promise<void>), updating references in
preOptimizeDevServer accordingly.

In `@e2e/react-start/hmr/tests/app.spec.ts`:
- Around line 13-20: In replaceRouteText, the current source.replace(from, to)
only changes the first occurrence and can silently edit the wrong spot; instead
read the routeFile source, count occurrences of the from token and if count ===
0 throw the existing error, if count > 1 throw a new error indicating the token
is ambiguous, and only when count === 1 perform the replacement and writeFile;
reference replaceRouteText, routeFile, and the from/to parameters when making
this change.

In `@packages/router-plugin/src/core/code-splitter/compilers.ts`:
- Around line 858-869: The code passes the raw splitNodeMeta (sourced from the
module-level SPLIT_NODES_CONFIG) into compiler plugins, risking accidental
mutation across files; instead, create and pass a shallow clone (e.g., via
Object.assign({}, splitNodeMeta) or {...splitNodeMeta}) when calling each plugin
in the loop that invokes opts.compilerPlugins and their onSplitRouteProperty
callback so plugins receive an immutable copy; ensure the change is applied
where splitNodeMeta is supplied to onSplitRouteProperty in compilers.ts so the
original SPLIT_NODES_CONFIG entry is not mutated.

In
`@packages/router-plugin/tests/add-hmr/snapshots/react/multi-component`@true.tsx:
- Around line 23-53: The helper emits untyped parameters (oldRoute, newRoute,
route, node) causing implicit any under strict TS; annotate handleRouteUpdate
and its inner walkReplaceSegmentTree with explicit types derived from your
runtime shapes (e.g., use the Route type for oldRoute/newRoute/route and the
SegmentTree node type for node — you can import or declare a RouteLike and
SegmentNodeLike interface matching router.processedTree.segmentTree), and add
those types to the function signatures (handleRouteUpdate(oldRoute: RouteLike,
newRoute: RouteLike) and walkReplaceSegmentTree(route: RouteLike, node:
SegmentNodeLike) and any variables like filter as needed) so the snapshot emits
strictly-typed TS code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecc6131e-86e7-439e-a2f2-d9e2f3e92f17

📥 Commits

Reviewing files that changed from the base of the PR and between 21e39bd and 5932ab8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • .changeset/fair-llamas-tease.md
  • e2e/e2e-utils/src/devServerWarmup.ts
  • e2e/e2e-utils/src/index.ts
  • e2e/react-start/css-modules/tests/setup/global.setup.ts
  • e2e/react-start/dev-ssr-styles/tests/setup/global.setup.ts
  • e2e/react-start/hmr/.gitignore
  • e2e/react-start/hmr/package.json
  • e2e/react-start/hmr/playwright.config.ts
  • e2e/react-start/hmr/src/routeTree.gen.ts
  • e2e/react-start/hmr/src/router.tsx
  • e2e/react-start/hmr/src/routes/__root.tsx
  • e2e/react-start/hmr/src/routes/index.tsx
  • e2e/react-start/hmr/tests/app.spec.ts
  • e2e/react-start/hmr/tests/setup/global.setup.ts
  • e2e/react-start/hmr/tests/setup/global.teardown.ts
  • e2e/react-start/hmr/tsconfig.json
  • e2e/react-start/hmr/vite.config.ts
  • e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts
  • e2e/solid-start/css-modules/tests/setup/global.setup.ts
  • e2e/vue-start/css-modules/tests/setup/global.setup.ts
  • packages/router-plugin/src/core/code-splitter/compilers.ts
  • packages/router-plugin/src/core/code-splitter/plugins.ts
  • packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts
  • packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts
  • packages/router-plugin/src/core/code-splitter/types.ts
  • packages/router-plugin/src/core/utils.ts
  • packages/router-plugin/tests/add-hmr.test.ts
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/test-files/react/multi-component.tsx
  • packages/router-plugin/tests/utils.test.ts

Comment on lines +13 to +20
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the repo’s canonical workspace specifier for internal packages.

These internal deps are declared as workspace:^, but the repo convention is workspace:*. Keeping them consistent avoids workspace manifest churn and matches the rest of the repo’s package rules.

🔧 Proposed fix
-    "@tanstack/react-router": "workspace:^",
-    "@tanstack/react-start": "workspace:^",
+    "@tanstack/react-router": "workspace:*",
+    "@tanstack/react-start": "workspace:*",
@@
-    "@tanstack/router-e2e-utils": "workspace:^",
+    "@tanstack/router-e2e-utils": "workspace:*",

Based on learnings, "Applies to package.json : Use workspace protocol for internal dependencies (workspace:*)"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"@tanstack/react-router": "workspace:*",
"@tanstack/react-start": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/hmr/package.json` around lines 13 - 20, The package.json uses
the non-canonical internal workspace specifier `workspace:^` for internal
packages; update the dependency entries for "@tanstack/react-router",
"@tanstack/react-start" and "@tanstack/router-e2e-utils" (and any other internal
deps using `workspace:^`) to use the canonical `workspace:*` specifier to match
repo conventions and avoid manifest churn.

Comment on lines +1 to +4
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the import order before lint blocks this file.

ESLint is already reporting import/order here. Move the node: imports above the package imports.

♻️ Proposed fix
-import { expect } from '@playwright/test'
-import { test } from '@tanstack/router-e2e-utils'
 import { readFile, writeFile } from 'node:fs/promises'
 import path from 'node:path'
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
🧰 Tools
🪛 ESLint

[error] 3-3: node:fs/promises import should occur before import of @playwright/test

(import/order)


[error] 4-4: node:path import should occur before import of @playwright/test

(import/order)

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

In `@e2e/react-start/hmr/tests/app.spec.ts` around lines 1 - 4, ESLint
import/order is violated: move the built-in Node "node:" imports (readFile,
writeFile from 'node:fs/promises' and path from 'node:path') above the external
package imports (expect from '@playwright/test' and test from
'@tanstack/router-e2e-utils'); reorder the import statements so the node:
imports come first to satisfy import/order (locate the imports at the top of
app.spec.ts where readFile, writeFile, path, expect, and test are declared).

Comment on lines +37 to +43
if (!t.isIdentifier(ctx.prop.key)) {
return
}

if (!REACT_STABLE_HMR_COMPONENT_IDENTS.has(ctx.prop.key.name)) {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle quoted route property keys too.

This early-return path skips semantically equivalent route options like { 'component': ... } or { 'errorComponent': ... }, so the new HMR preservation logic only applies to identifier keys.

🐛 Proposed fix
-      if (!t.isIdentifier(ctx.prop.key)) {
-        return
-      }
-
-      if (!REACT_STABLE_HMR_COMPONENT_IDENTS.has(ctx.prop.key.name)) {
+      const propName = t.isIdentifier(ctx.prop.key)
+        ? ctx.prop.key.name
+        : t.isStringLiteral(ctx.prop.key)
+          ? ctx.prop.key.value
+          : undefined
+
+      if (!propName || !REACT_STABLE_HMR_COMPONENT_IDENTS.has(propName)) {
         return
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!t.isIdentifier(ctx.prop.key)) {
return
}
if (!REACT_STABLE_HMR_COMPONENT_IDENTS.has(ctx.prop.key.name)) {
return
}
const propName = t.isIdentifier(ctx.prop.key)
? ctx.prop.key.name
: t.isStringLiteral(ctx.prop.key)
? ctx.prop.key.value
: undefined
if (!propName || !REACT_STABLE_HMR_COMPONENT_IDENTS.has(propName)) {
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts`
around lines 37 - 43, The early-return only allows identifier keys, skipping
quoted keys like 'component' or 'errorComponent'; update the check around
ctx.prop.key and REACT_STABLE_HMR_COMPONENT_IDENTS so it accepts both Identifier
and StringLiteral keys by extracting the key name (use ctx.prop.key.name for
Identifier and ctx.prop.key.value for StringLiteral) and testing that string
against REACT_STABLE_HMR_COMPONENT_IDENTS instead of relying solely on
t.isIdentifier; keep the existing t import checks but branch to handle string
literals before returning.

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/e2e-utils/src/devServerWarmup.ts (1)

1-1: Use type-only import for Page and fix alphabetical order.

ESLint reports that Page is only used as a type annotation (in the opts parameter on line 34). Split it into a separate type import to satisfy @typescript-eslint/consistent-type-imports and fix the alphabetical ordering.

♻️ Proposed fix
-import { chromium, Page } from '@playwright/test'
+import { chromium } from '@playwright/test'
+import type { Page } from '@playwright/test'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/e2e-utils/src/devServerWarmup.ts` at line 1, Split the combined import so
that Page is imported as a type-only import and ensure the named imports are in
alphabetical order: replace the single line "import { chromium, Page } from
'@playwright/test'" with two imports—one value import for chromium and one type
import for Page—so the type-only import satisfies
`@typescript-eslint/consistent-type-imports` and the named imports are
alphabetized; update references to Page used in the opts parameter (function
with parameter named opts) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/e2e-utils/src/devServerWarmup.ts`:
- Line 1: Split the combined import so that Page is imported as a type-only
import and ensure the named imports are in alphabetical order: replace the
single line "import { chromium, Page } from '@playwright/test'" with two
imports—one value import for chromium and one type import for Page—so the
type-only import satisfies `@typescript-eslint/consistent-type-imports` and the
named imports are alphabetized; update references to Page used in the opts
parameter (function with parameter named opts) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 525dc1dc-4c4f-442a-aa04-7d0984859363

📥 Commits

Reviewing files that changed from the base of the PR and between 5932ab8 and 48061d5.

📒 Files selected for processing (4)
  • e2e/e2e-utils/src/devServerWarmup.ts
  • e2e/react-start/hmr/src/routes/index.tsx
  • e2e/react-start/hmr/tests/app.spec.ts
  • e2e/react-start/hmr/tests/setup/global.setup.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/react-start/hmr/tests/setup/global.setup.ts

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 20, 2026

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing fix-react-hmr (e462090) with main (91cc628)1

Open in CodSpeed

Footnotes

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

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/router-plugin/tests/add-hmr/test-files/react/string-literal-keys.tsx`:
- Around line 5-9: The fixture uses identifier property keys (loader, component,
errorComponent) so it doesn't exercise quoted-key parsing; update the object
passed to createFileRoute('/posts') so those properties are written as
string-literal keys — e.g. use "loader": fetchPosts, "component":
PostsComponent, and "errorComponent": PostsErrorComponent while keeping the same
values and Route/createFileRoute call intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89ef47de-c091-4685-bdc3-a4dc8caaf007

📥 Commits

Reviewing files that changed from the base of the PR and between 48061d5 and 6f0bd55.

📒 Files selected for processing (6)
  • e2e/e2e-utils/src/devServerWarmup.ts
  • packages/router-plugin/src/core/code-splitter/compilers.ts
  • packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/snapshots/react/[email protected]
  • packages/router-plugin/tests/add-hmr/test-files/react/string-literal-keys.tsx
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/router-plugin/src/core/code-splitter/compilers.ts
  • e2e/e2e-utils/src/devServerWarmup.ts

Comment on lines +5 to +9
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: PostsComponent,
errorComponent: PostsErrorComponent,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use actual string-literal property keys in this fixture.

Line 7 and Line 8 currently use identifier keys, so this fixture does not directly exercise quoted-key input behavior despite its purpose/name.

✅ Suggested fixture update
 export const Route = createFileRoute('/posts')({
   loader: fetchPosts,
-  component: PostsComponent,
-  errorComponent: PostsErrorComponent,
+  'component': PostsComponent,
+  'errorComponent': PostsErrorComponent,
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: PostsComponent,
errorComponent: PostsErrorComponent,
})
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
'component': PostsComponent,
'errorComponent': PostsErrorComponent,
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/router-plugin/tests/add-hmr/test-files/react/string-literal-keys.tsx`
around lines 5 - 9, The fixture uses identifier property keys (loader,
component, errorComponent) so it doesn't exercise quoted-key parsing; update the
object passed to createFileRoute('/posts') so those properties are written as
string-literal keys — e.g. use "loader": fetchPosts, "component":
PostsComponent, and "errorComponent": PostsErrorComponent while keeping the same
values and Route/createFileRoute call intact.

@schiller-manuel schiller-manuel merged commit 9351e99 into main Mar 21, 2026
16 checks passed
@schiller-manuel schiller-manuel deleted the fix-react-hmr branch March 21, 2026 00:27
@coderabbitai coderabbitai bot mentioned this pull request Mar 21, 2026
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