Skip to content

test(router): guard Fast Refresh for co-located non-"use client" file (vite-plugin-react#1248) - #574

Merged
ivogt merged 1 commit into
mainfrom
test/colocated-fast-refresh-hmr-guard
Jun 13, 2026
Merged

ivogt merged 1 commit into
mainfrom
test/colocated-fast-refresh-hmr-guard

Conversation

@ivogt

@ivogt ivogt commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

@vitejs/plugin-rsc (which Rango builds on) has a dev-only hotUpdate guard that, for a file present in the rsc module graph but not behind a "use client" boundary, returns [] and drops client Fast Refresh. Upstream PR vitejs/vite-plugin-react#1248 ("keep client HMR for client modules co-located with rsc-graph code", currently open/unmerged) narrows that guard so a genuinely client-rendered component co-located with server-graph code keeps Fast Refresh.

Does Rango need that fix? No — Rango's client module graph is only ever reached through "use client" client references (Flight hydration; the browser entry imports only framework internals), so isInsideClientBoundary is always true for those modules and the guard never fires. But that invariant was untested: no e2e exercised a non-"use client" file living in both graphs. This PR closes the gap so a future plugin-rsc bump can't silently regress Fast Refresh for the co-located pattern.

Consumer-observable framing: a consumer who co-locates a server export and a client-rendered component in one non-"use client" file, consumed by a "use client" island, edits that file in dev and expects Fast Refresh — state preserved, no full reload. This PR pins that.

The pattern this pins (consumer seat)

// ColocatedFrShared.tsx — no "use client". Every export is a component, so the
// module stays a valid React Refresh boundary.
export function ColocatedFrMarker() {        // rendered by the "use client" island
  return <span data-testid="colocated-fr-marker">marker-baseline</span>;
}
export function ColocatedFrServerNote() {     // rendered by the server route
  return <p data-testid="colocated-fr-server-note">server-note</p>;
}
// ColocatedFrIsland.tsx — "use client" island. Its boundary is what keeps Fast Refresh.
"use client";
import { useState } from "react";
import { ColocatedFrMarker } from "./ColocatedFrShared.js";
export function ColocatedFrIsland() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <ColocatedFrMarker />
      <button onClick={() => setCount((c) => c + 1)}>count: {count}</button>
    </div>
  );
}
// colocated-fast-refresh.tsx — server route. Importing ColocatedFrServerNote puts the
// shared file in the rsc graph; the island renders the marker on the client.
import { ColocatedFrServerNote } from "../components/ColocatedFrShared.js";
import { ColocatedFrIsland } from "../components/ColocatedFrIsland.js";

Editing marker-baseline -> marker-edited in dev: the marker updates and the island counter stays at its seeded value (Fast Refresh, not a remount/reload).

Semantics

File situation "use client"? client-graph importer plugin-rsc client guard fires? Fast Refresh
rsc graph only (server route component) no none (not in client graph) n/a (rsc branch) server re-render
both graphs, reached via island no "use client" island (client reference) no — isInsideClientBoundary true preserved (this PR)
both graphs, CSR-island chain, no client ref anywhere no plain module, no client reference yes (0.5.26) dropped upstream; fixed by #1248
"use client" component yes — n/a preserved (React Refresh; Rango's client-component-hmr plugin cedes rsc/ssr)

Rango only ever produces row 2 (boundary present), never row 3 (the CSR-island shape #1248 fixes), because the browser entry hydrates Flight and never statically imports route/component modules.

Changes

Test-only + test-app fixtures; no router src, no public API, no bundle impact.

  • e2e/test-app/src/components/ColocatedFrShared.tsx (new) — dual-graph, non-"use client", all-component exports.
  • e2e/test-app/src/components/ColocatedFrIsland.tsx (new) — "use client" island + counter.
  • e2e/test-app/src/urls/colocated-fast-refresh.tsx (new) — server route at /colocated-fr.
  • e2e/test-app/src/urls.tsx, e2e/test-app/src/router.named-routes.gen.ts — wire + regenerate the route.
  • e2e/colocated-fast-refresh.test.ts (new) — render/hydration in dev + production.
  • e2e/client-component-hmr.test.ts — new colocated-fast-refresh-hmr describe (dev-only HMR).

Test plan

  • pnpm run typecheck — 12/12 packages
  • pnpm run test:unit:all (unit + RSC, all packages) — green
  • pnpm run lint (oxlint) — 0 warnings, 0 errors
  • pnpm run format (oxfmt) — clean
  • pnpm check:e2e-bucketing — OK
  • e2e colocated-fast-refresh (dev) + (production) render — pass
  • e2e colocated-fast-refresh-hmr (hmr-client) — pass (marker updates, island counter survives, no reload)

Contract pinned: the HMR test pins that editing a non-"use client" dual-graph file keeps client Fast Refresh (seeded state preserved); the render twin pins that the same fixture builds + hydrates in a production build.

Notes

Pins that editing a non-"use client" file present in both the rsc and client
module graphs (a server export plus a client-rendered component, consumed by a
"use client" island) keeps client Fast Refresh: the marker text updates while
seeded island state survives, with no full reload.

This is the Rango-safe analog of vitejs/vite-plugin-react#1248. The island's
"use client" boundary makes isInsideClientBoundary true, so plugin-rsc's client
hotUpdate guard never fires for the shared file. Adds a dedicated /colocated-fr
fixture route, a dev-only HMR test in client-component-hmr.test.ts, and dev plus
production render coverage.
@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@574

commit: 34f3863

@ivogt
ivogt merged commit 1fa8996 into main Jun 13, 2026
25 checks passed
@ivogt
ivogt deleted the test/colocated-fast-refresh-hmr-guard branch June 13, 2026 18:46
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