Skip to content

fix: pre-bundle client packages to avoid duplicate React on cold start#129

Closed
uhyo wants to merge 1 commit into
masterfrom
claude/issue-128-zs21qb
Closed

fix: pre-bundle client packages to avoid duplicate React on cold start#129
uhyo wants to merge 1 commit into
masterfrom
claude/issue-128-zs21qb

Conversation

@uhyo

@uhyo uhyo commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #128 — on a cold-started dev server (no .vite cache), React was loaded twice on the very first request, producing an Invalid hook call error before Vite auto-reloaded the page. On the second request (deps already optimized) the problem disappeared.

Root cause

@vitejs/plugin-rsc treats any package that declares react as a peer dependency as a "framework package." When a server component imports one, the plugin reaches it on the browser through a client-package-proxy virtual module that is generated at request time. The dependency optimizer's initial scan cannot follow that virtual module, so on a cold server the package — and the copy of React it pulls in — is only discovered once the page requests it.

That late discovery triggers a re-optimization pass, which leaves a second copy of React loaded on the first render before Vite reloads. The mismatched React instances break hooks (Invalid hook callCannot read properties of null (reading 'useState')).

This is not specific to @funstack/router (the package fsRoutes mode renders through) — any third-party "use client" component library imported from a server component hits the same wall.

Fix

Detect the project's directly-declared client packages — dependencies that list react in peerDependencies, the same heuristic @vitejs/plugin-rsc itself uses to decide what to proxy — and add them to the browser environment's optimizeDeps.include. They are then pre-bundled during the optimizer's initial pass, so React stays deduplicated to a single optimized chunk from the first request. No re-optimization, no duplicate React, no auto-reload.

Notes:

  • Scoped to direct dependencies — those are the bare specifiers a server component can import and that Vite can resolve from the project root. A framework package nested inside another dependency is pre-bundled together with its parent. (Using the full transitive crawl instead surfaced internal deps like react-error-boundary that aren't resolvable from the user's root, causing Failed to resolve dependency warnings.)
  • The react-in-peerDependencies key correctly excludes build plugins such as @vitejs/plugin-react (which peer-depends on vite, not react), so there are no false positives.
  • The previous hardcoded @funstack/router special-case is removed — it is now auto-detected like any other client package, in all entry modes.

Verification

  • Reproduced the cold-start Invalid hook call reliably before the fix (4/4 runs); after the fix multiple cold starts are clean (no error, no auto-reload) for both fsRoutes/@funstack/router and an arbitrary third-party-style "use client" package — with zero user config.
  • Multi-entry and single-entry modes start without resolve warnings and render correctly.
  • New unit tests for the detection (clientPackages.test.ts, fixture-based) plus an updated index.test.ts. Full unit suite, typecheck, lint, and format:check all pass.

🤖 Generated with Claude Code


Generated by Claude Code

When a server component imports a "use client" package — one that declares
`react` as a peer dependency, such as @funstack/router or a third-party
component library — @vitejs/plugin-rsc reaches it on the browser through a
`client-package-proxy` virtual module generated at request time. The
dependency optimizer's initial scan cannot follow that virtual module, so on
a cold-started dev server the package (and the copy of React it pulls in) is
only discovered once the page requests it.

That late discovery triggers a re-optimization pass, which leaves a second
copy of React loaded on the very first render before Vite reloads the page.
The mismatched React instances broke hooks with an "Invalid hook call" error.

Detect the project's directly-declared client packages (the same
`react`-peer-dependency heuristic @vitejs/plugin-rsc uses to decide which
packages to proxy) and add them to the browser environment's
`optimizeDeps.include`, so they are pre-bundled during the initial pass and
React stays deduplicated to a single optimized chunk from the first request.

Fixes #128

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DADrMq65BFa1Qv9Dw1YG4c
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.

React loads twice in first load from cold started dev server

2 participants