fix: pre-bundle client packages to avoid duplicate React on cold start#129
Closed
uhyo wants to merge 1 commit into
Closed
fix: pre-bundle client packages to avoid duplicate React on cold start#129uhyo wants to merge 1 commit into
uhyo wants to merge 1 commit into
Conversation
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
This was referenced Jun 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #128 — on a cold-started dev server (no
.vitecache), React was loaded twice on the very first request, producing anInvalid hook callerror before Vite auto-reloaded the page. On the second request (deps already optimized) the problem disappeared.Root cause
@vitejs/plugin-rsctreats any package that declaresreactas a peer dependency as a "framework package." When a server component imports one, the plugin reaches it on the browser through aclient-package-proxyvirtual 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 call→Cannot 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
reactinpeerDependencies, the same heuristic@vitejs/plugin-rscitself uses to decide what to proxy — and add them to the browser environment'soptimizeDeps.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:
react-error-boundarythat aren't resolvable from the user's root, causingFailed to resolve dependencywarnings.)react-in-peerDependencieskey correctly excludes build plugins such as@vitejs/plugin-react(which peer-depends onvite, notreact), so there are no false positives.@funstack/routerspecial-case is removed — it is now auto-detected like any other client package, in all entry modes.Verification
Invalid hook callreliably before the fix (4/4 runs); after the fix multiple cold starts are clean (no error, no auto-reload) for both fsRoutes/@funstack/routerand an arbitrary third-party-style"use client"package — with zero user config.clientPackages.test.ts, fixture-based) plus an updatedindex.test.ts. Full unit suite,typecheck,lint, andformat:checkall pass.🤖 Generated with Claude Code
Generated by Claude Code