Skip to content

feat: state-preserving HMR and hot data revalidation in classic dev#93

Merged
ScriptedAlchemy merged 7 commits into
mainfrom
feat/classic-dev-hmr
Jul 10, 2026
Merged

feat: state-preserving HMR and hot data revalidation in classic dev#93
ScriptedAlchemy merged 7 commits into
mainfrom
feat/classic-dev-hmr

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Collaborator

Summary

Ports the classic-mode (non-RSC) dev HMR feature from the React Router 8 branch (#74) so RR7 users get it without waiting for RR8:

  • State-preserving HMR: route module edits in dev now re-register components with React Refresh and patch routes/matches in place (src/dev-hmr.ts runtime delivered as a virtual module imported by each route's client entry) instead of forcing a full reload. Component state survives edits.
  • Hot data revalidation (HDR): committed node rebuilds bump a .react-router/hdr-revision.mjs file watched by the web compiler; the client runtime revalidates the router so loader data refreshes without a reload.
  • React Refresh registration backfill: pre-lowered routes (e.g. MDX) whose JSX was compiled before SWC's refresh transform now get $RefreshReg$ registrations following react-refresh's own component-detection rules.
  • Degrades cleanly to the previous full-reload behavior when @rsbuild/plugin-react isn't present.

Bug found & fixed during verification (also affects #74)

Live-testing surfaced an infinite rebuild loop: web-only commits reuse the node compiler's stale modifiedFiles, and each HDR bump itself triggers a web rebuild — so after the first server-file edit the bump signal self-sustained (revision file climbed unboundedly, constant CPU burn). Reproduced identically on the source branch. Fixed by signaling once per node compilation identity; the same fix is pushed to #74. Regression test included (red-green verified).

Testing

  • tsc --noEmit clean; full rstest suite 462/462.
  • New unit tests: HDR revision signal, HMR runtime module generation, refresh-runtime resolution fallback; 10 ported backfill parity tests; node-rebuild signaling tests incl. the loop regression.
  • Live smoke on examples/default-template (RR 7.14): page 200, HMR runtime present in route bundles only, HDR revision bumps exactly once per server edit and stays stable (0 → 1, steady for 75s).
  • Verified the private router APIs the runtime uses (createRoutesForHMR, _internalSetRoutes, etc.) exist in react-router 7.13.0 (the dependency floor) via HydratedRouter, which the plugin's own entry.client uses.

Includes the feature changeset (minor) and the refresh-backfill changeset (patch).

🤖 Generated with Claude Code

Ports the classic-mode (non-RSC) dev HMR feature from the RR8-compat
branch onto main: route module updates in development now apply React
Refresh registration and in-place route patching instead of forcing a
full reload, and server code changes bump an HDR revision file that
signals hot data revalidation to the client. Degrades to the previous
full-reload behavior when @rsbuild/plugin-react is not present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b9a4a476a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/dev-generation.ts Outdated
previous.web.manifestsByEntryName,
manifestsByEntryName
manifestsByEntryName,
!clientPatchesRouteMetadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't suppress reloads for unloaded routes

When a lazily loaded/unvisited route changes only HMR-patchable flags such as hasLoader or hasClientLoader, its route client entry is not in the browser module graph, so the new HMR runtime never runs scheduleReactRouterRouteUpdate for that route. Passing false here suppresses the previous full reload globally, leaving window.__reactRouterManifest with stale metadata until a manual reload; a later navigation can then skip a newly added loader or fetch one that was removed. Keep the full reload for metadata changes that cannot be patched by an already-loaded route module.

Useful? React with 👍 / 👎.

Comment thread src/index.ts Outdated
})
: undefined;
devHdrSignal?.ensure();
const devHmrEnabled = devHmrRefreshRuntimePath !== undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate route HMR on enabled Fast Refresh

This enables the new self-accepting route HMR whenever the package can resolve @rsbuild/plugin-react, even for configs that install the plugin but disable Fast Refresh (for example the federation examples use pluginReact({ fastRefresh: false, swcReactOptions: { refresh: false } })). In that case the React Refresh plugin/transform is not installed, but route updates are still self-accepted and applyRouteModuleUpdate preserves the old component identity, so component-only edits can stop updating instead of falling back to a full reload. Please base this on the actual enabled React Refresh configuration rather than package resolution alone.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ScriptedAlchemy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2162f509-867a-4df9-ad14-49960f2a1bbb

📥 Commits

Reviewing files that changed from the base of the PR and between d5177b6 and b2bd325.

📒 Files selected for processing (4)
  • src/dev-hmr.ts
  • src/index.ts
  • tests/dev-hmr-fast-refresh-detection.test.ts
  • tests/dev-hmr.test.ts
📝 Walkthrough

Walkthrough

Adds development-only React Router HMR with React Refresh registration, route metadata updates, in-place route patching, and loader-data revalidation. Route transforms now support HMR flags, route IDs, and registration backfill for pre-lowered modules. A generated browser runtime uses HDR revision files to receive rebuild signals. Dev runtime orchestration signals qualifying node rebuilds while excluding CSS and HDR-only changes. Tests and changesets cover the new behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main dev HMR and hot data revalidation changes.
Description check ✅ Passed The description matches the implemented classic dev HMR, HDR, backfill, and regression-fix work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/classic-dev-hmr

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/rsbuild-plugin-react-router@b2bd325

commit: b2bd325

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Compared PR head b2bd325 against base c9535d8.

Dev Rollup

Group Fixtures Base total Head total Delta Base ready Head ready Ready delta Base routes Head routes Route delta Base update/HMR Head update/HMR Update delta Speedup
All dev fixtures 7 29.93s 32.58s +8.9% 19.94s 20.02s +0.4% 4.05s 4.10s +1.3% 3.31s 3.01s -9.0% 0.92x
Large app 1 13.97s 16.09s +15.2% 8.49s 8.62s +1.5% 2.02s 2.00s -0.7% 1.80s 1.83s +1.7% 0.87x
Standard fixtures 6 15.96s 16.49s +3.3% 11.45s 11.41s -0.4% 2.03s 2.09s +3.3% 1.52s 1.19s -21.8% 0.97x

Production Build Benchmarks

Rendered 7 production build benchmarks.

Benchmark Runs Base total Head total Delta Head mean Head p95 Speedup Head RSS p95
large-355-ssr-esm 5 9.02s 9.02s -0.0% 9.00s 9.23s 1.00x 1535 MB
synthetic-1024-ssr-esm 5 4.25s 4.27s +0.4% 4.27s 4.48s 1.00x 637 MB
synthetic-1024-ssr-esm-split 5 5.72s 5.63s -1.4% 5.63s 5.79s 1.01x 818 MB
synthetic-256-sourcemaps 10 2.24s 2.23s -0.7% 2.23s 2.41s 1.01x 448 MB
synthetic-256-ssr-esm 10 2.13s 2.09s -1.6% 2.11s 2.26s 1.02x 425 MB
synthetic-256-ssr-esm-split 10 2.51s 2.50s -0.3% 2.52s 2.67s 1.00x 450 MB
synthetic-48-ssr-esm 10 1.41s 1.35s -4.6% 1.37s 1.59s 1.05x 329 MB

full Dev Fixture Summary

Rendered 7 dev benchmark fixtures from the full profile.

Benchmark Runs Base total Head total Delta Base ready Head ready Base routes Head routes Base update/HMR Head update/HMR Update delta Head mean Head p95 Speedup Head RSS p95
large-355-ssr-esm 5 13.97s 16.09s +15.2% 8.49s 8.62s 2.02s 2.00s 1.80s 1.83s +1.7% 16.22s 16.98s 0.87x -
synthetic-1024-ssr-esm 5 4.68s 4.82s +3.0% 3.36s 3.32s 0.59s 0.59s 0.48s 0.35s -26.4% 4.84s 4.94s 0.97x -
synthetic-1024-ssr-esm-split 5 4.76s 4.89s +2.7% 3.37s 3.37s 0.58s 0.61s 0.53s 0.35s -33.4% 4.84s 4.96s 0.97x -
synthetic-256-sourcemaps 10 2.03s 2.10s +3.8% 1.51s 1.51s 0.25s 0.25s 0.15s 0.15s -0.4% 2.09s 2.14s 0.96x -
synthetic-256-ssr-esm 10 1.80s 1.87s +4.0% 1.27s 1.29s 0.24s 0.26s 0.15s 0.13s -16.4% 1.86s 1.95s 0.96x -
synthetic-256-ssr-esm-split 10 1.78s 1.85s +3.5% 1.29s 1.27s 0.23s 0.26s 0.15s 0.13s -17.0% 1.84s 1.93s 0.97x -
synthetic-48-ssr-esm 10 0.91s 0.96s +5.5% 0.65s 0.65s 0.13s 0.13s 0.05s 0.08s +47.0% 0.96s 0.98s 0.95x -

large-355-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 1785 1720.6ms 1854.6ms +7.8% 1854.6ms 20.4ms 10
node route:module 1785 847.2ms 898.0ms +6.0% 898.0ms 6.5ms 10
web route:client-entry 1785 374.3ms 446.5ms +19.3% 446.5ms 5.2ms 10
node manifest:transform 5 107.3ms 181.5ms +69.2% 181.5ms 66.9ms 5
web manifest:stage 15 16.0ms 20.5ms +28.1% 20.5ms 2.3ms 15
web manifest:transform 5 0.5ms 0.5ms 0.0% 0.5ms 0.1ms 5

synthetic-1024-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 5130 2111.1ms 2079.7ms -1.5% 2079.7ms 17.1ms 10
node route:module 5130 977.0ms 946.5ms -3.1% 946.5ms 5.2ms 10
web route:client-entry 5130 647.5ms 689.3ms +6.5% 689.3ms 11.7ms 10
node manifest:transform 5 211.3ms 207.4ms -1.8% 207.4ms 42.5ms 5
node module:client-only-stub 5 201.8ms 112.3ms -44.4% 112.3ms 47.9ms 5
web manifest:stage 15 57.2ms 71.5ms +25.0% 71.5ms 8.0ms 15
web manifest:transform 5 0.5ms 0.5ms 0.0% 0.5ms 0.1ms 5

synthetic-1024-ssr-esm-split Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 5130 2135.0ms 2112.1ms -1.1% 2112.1ms 20.0ms 10
node route:module 5130 973.5ms 957.9ms -1.6% 957.9ms 6.1ms 10
web route:client-entry 5130 645.9ms 706.9ms +9.4% 706.9ms 7.1ms 10
node module:client-only-stub 5 274.8ms 92.3ms -66.4% 92.3ms 27.5ms 5
node manifest:transform 5 211.3ms 210.9ms -0.2% 210.9ms 45.1ms 5
web manifest:stage 15 60.0ms 64.0ms +6.7% 64.0ms 8.2ms 15
web manifest:transform 5 0.5ms 0.5ms 0.0% 0.5ms 0.1ms 5

synthetic-256-sourcemaps Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2580 1420.5ms 1501.2ms +5.7% 1501.2ms 24.7ms 20
node route:module 2580 607.0ms 638.4ms +5.2% 638.4ms 8.5ms 20
web route:client-entry 2580 407.4ms 426.7ms +4.7% 426.7ms 6.2ms 20
node module:client-only-stub 10 253.9ms 312.7ms +23.2% 312.7ms 72.9ms 10
node manifest:transform 10 159.8ms 144.0ms -9.9% 144.0ms 18.8ms 10
web manifest:stage 30 25.0ms 31.5ms +26.0% 31.5ms 3.2ms 30
web manifest:transform 10 1.0ms 1.0ms 0.0% 1.0ms 0.1ms 10

synthetic-256-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2580 1410.1ms 1431.2ms +1.5% 1431.2ms 18.7ms 20
node route:module 2580 561.8ms 551.4ms -1.9% 551.4ms 5.4ms 20
web route:client-entry 2580 394.8ms 422.9ms +7.1% 422.9ms 5.2ms 20
node manifest:transform 10 175.7ms 173.0ms -1.5% 173.0ms 23.7ms 10
node module:client-only-stub 10 159.0ms 51.0ms -67.9% 51.0ms 17.3ms 10
web manifest:stage 30 22.3ms 29.1ms +30.5% 29.1ms 1.4ms 30
web manifest:transform 10 1.0ms 1.0ms 0.0% 1.0ms 0.1ms 10

synthetic-256-ssr-esm-split Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2580 1385.1ms 1430.3ms +3.3% 1430.3ms 22.8ms 20
node route:module 2580 550.5ms 571.4ms +3.8% 571.4ms 5.0ms 20
web route:client-entry 2580 398.3ms 429.6ms +7.9% 429.6ms 5.6ms 20
node manifest:transform 10 175.2ms 163.6ms -6.6% 163.6ms 21.9ms 10
node module:client-only-stub 10 81.4ms 225.3ms +176.8% 225.3ms 67.3ms 10
web manifest:stage 30 22.4ms 29.3ms +30.8% 29.3ms 1.4ms 30
web manifest:transform 10 1.1ms 1.0ms -9.1% 1.0ms 0.1ms 10

synthetic-48-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 500 481.7ms 480.0ms -0.4% 480.0ms 12.9ms 20
node route:module 500 169.5ms 177.6ms +4.8% 177.6ms 4.8ms 20
web route:client-entry 500 107.8ms 128.6ms +19.3% 128.6ms 3.7ms 20
node module:client-only-stub 10 73.8ms 81.7ms +10.7% 81.7ms 11.4ms 10
node manifest:transform 10 54.2ms 55.6ms +2.6% 55.6ms 7.7ms 10
web manifest:stage 30 5.5ms 7.7ms +40.0% 7.7ms 0.4ms 30
web manifest:transform 10 1.0ms 1.1ms +10.0% 1.1ms 0.2ms 10

Synthetic Rsbuild App

Rendered 2 production build benchmarks.

Benchmark Runs Base total Head total Delta Head mean Head p95 Speedup Head RSS p95
complex app 2 114.87s 117.91s +2.6% 117.91s - 0.97x -
complex app 2 80.90s 88.19s +9.0% 88.19s - 0.92x -

Rendered 1 dev benchmark fixture from the embedded complex app.

Benchmark Runs Base total Head total Delta Base ready Head ready Base routes Head routes Base update/HMR Head update/HMR Update delta Head mean Head p95 Speedup Head RSS p95
complex app 2 99.66s 111.65s +12.0% 90.81s 98.03s 2.92s 3.12s 3.39s 3.73s +10.2% 111.65s - 0.89x -

Profile: full; mode: dev; iterations: 10; warmup: 0.
The uploaded benchmark artifact includes diagnostics/summary.md and diagnostics/summary.json with runner metadata, per-run timing samples, CPU/RSS samples, and plugin timing hot spots.
Workflow run

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/dev-hmr.ts (1)

383-392: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Revalidation navigate fallback is bypassed when router.revalidate is absent.

When shouldRefreshRouteState is true and revalidation is required, refreshRouteState(router) runs and shouldRevalidate is set to false. But refreshRouteState is a no-op unless router.revalidate is a function, whereas revalidateRouter additionally falls back to a navigate(...) replace. So on a router without revalidate, loader data that should be revalidated never is, because the fallback path is skipped. This is only reachable if the data router lacks revalidate, so please confirm whether that shape can occur in the supported React Router versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-hmr.ts` around lines 383 - 392, Ensure revalidation is not marked
complete when router.revalidate is unavailable: inspect refreshRouteState and
revalidateRouter, and only clear shouldRevalidate after a successful refresh
that supports router.revalidate; otherwise allow revalidateRouter’s navigate
replace fallback to run. Confirm compatibility with supported React Router
router shapes and preserve the fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/dev-hmr.ts`:
- Around line 383-392: Ensure revalidation is not marked complete when
router.revalidate is unavailable: inspect refreshRouteState and
revalidateRouter, and only clear shouldRevalidate after a successful refresh
that supports router.revalidate; otherwise allow revalidateRouter’s navigate
replace fallback to run. Confirm compatibility with supported React Router
router shapes and preserve the fallback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c1c66d8d-17ea-4a8b-ab1a-dbe7bb33fda5

📥 Commits

Reviewing files that changed from the base of the PR and between 85eb78a and d5177b6.

📒 Files selected for processing (9)
  • src/dev-hmr.ts
  • src/dev-runtime-controller.ts
  • src/index.ts
  • src/prerender-build.ts
  • src/route-artifacts.ts
  • src/route-transform-tasks.ts
  • tests/dev-hmr-fast-refresh-detection.test.ts
  • tests/route-artifacts.test.ts
  • tests/setup.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/route-artifacts.test.ts
  • src/route-transform-tasks.ts
  • src/dev-runtime-controller.ts
  • src/index.ts
  • src/route-artifacts.ts

@ScriptedAlchemy ScriptedAlchemy merged commit 2c155f2 into main Jul 10, 2026
8 checks passed
@ScriptedAlchemy ScriptedAlchemy deleted the feat/classic-dev-hmr branch July 10, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant