Skip to content

fix(rsc): hot module replacement of CSS for nested RSC with cssLinkPrecedence: false #1188

Open
jantimon wants to merge 6 commits intovitejs:mainfrom
jantimon:failing-test/rsc-nested-css-hmr
Open

fix(rsc): hot module replacement of CSS for nested RSC with cssLinkPrecedence: false #1188
jantimon wants to merge 6 commits intovitejs:mainfrom
jantimon:failing-test/rsc-nested-css-hmr

Conversation

@jantimon
Copy link
Copy Markdown

@jantimon jantimon commented Apr 15, 2026

I saw a bug in CSS HMR when trying out RSC in Tan Stack start https://tanstack.com/blog/react-server-components

Actually I wanted to see if RSC works well in TanStack and allows the css of next-yak static css-in-js plugin to be hot reloaded: DigitecGalaxus/next-yak#529

It seems that a server component whose module lives only in the rsc Vite environment (not in the client bundle) is rendered through a nested RSC Flight stream.

What the bug does

Editing a CSS file that is only imported from a Server Component rendered through a nested RSC Flight stream does not reliably update the page. The first edit in a dev session often appears to work (via a Vite side-effect), but any subsequent edit in the same session is silently ignored. So every HMR message after that sits behind a hung promise on the client. Users see the page stuck on edit‑1 CSS until a full page reload or dev-server restart.

Scope / who is affected

  • Frameworks that set cssLinkPrecedence: false e.g TanStack Start.
  • Frameworks that render RSC through nested Flight streams (createServerFn + renderServerComponent, etc.).
  • The default path (cssLinkPrecedence: true, React 19 Float-managed) is unaffected — the fix is narrowly gated so it does not change that path.

The three-part fix (in plugin-rsc/src/plugin.ts)

  1. Invalidate the derived CSS virtual in hotUpdate (rsc env). Walk the importer chain from the changed CSS and invalidate only the \0virtual:vite-rsc/css?type=rsc&… modules. JS importers (inner.tsx, server.tsx, root.tsx) are intentionally left untouched to avoid regressing the Float-managed default path
  2. Cache-bust the emitted <link href> in collectCss. Only when cssLinkPrecedence: false and the consumer isn't already client, append ?t=<lastHMRTimestamp>. The default Float path stays bare-href so Vite's in-place <link> swap keeps working
  3. Skip Vite client CSS HMR for RSC-only CSS in the client env's hotUpdate. When cssLinkPrecedence: false and the changed CSS has no client-side JS importer, return []. This is what prevents Vite's Promise.all from hanging on a React-owned <link> that gets unmounted mid-swap.. and that hang was the reason only the first edit ever appeared to work

@hi-ogawa
Copy link
Copy Markdown
Contributor

Hey thanks for digging.
Is this reproducible only with cssLinkPrecedence: false? That's added for tanstack for their need as experimental so we don't test on our side nor haven't properly reviewed how it works. cc @schiller-manuel

Also, if you have a fix, please feel free to push here to verify the fix.

@jantimon jantimon changed the title CSS HMR breaks for RSC-only server components rendered via a nested Flight stream fix(rsc): CSS HMR for nested RSC with cssLinkPrecedence: false Apr 17, 2026
@jantimon jantimon changed the title fix(rsc): CSS HMR for nested RSC with cssLinkPrecedence: false fix(rsc): hot module replacement of CSS for nested RSC with cssLinkPrecedence: false Apr 17, 2026
@jantimon
Copy link
Copy Markdown
Author

This pr fixes the bug in vite-plugin-react rsc for cssLinkPrecedence: false:

vite-hmr.mp4

However it introduces a new issue for the tanstack integration where we will need the help of @schiller-manuel to find the best approach. The problem is that it does not fix the removal of css props as old css styles now stay in DOM because of the cache busting ?t=...

stylesheet links stay in dom

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.

2 participants