fix(rsc): fix server css hmr with cssLinkPrecedence: false#1188
fix(rsc): fix server css hmr with cssLinkPrecedence: false#1188hi-ogawa merged 16 commits intovitejs:mainfrom
cssLinkPrecedence: false#1188Conversation
|
Hey thanks for digging. Also, if you have a fix, please feel free to push here to verify the fix. |
|
This pr fixes the bug in vite-plugin-react rsc for vite-hmr.mp4However 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
|
commit: |
|
I should've triggered this sooner, but the preview release is available in #1188 (comment) Please test it out with tasntack router integration. I get the idea of changes, but need more time to review. Also will push some changes to test some ideas. |
| for (const imp of mod.importers) { | ||
| walk(imp) | ||
| } | ||
| } |
There was a problem hiding this comment.
This looks similar to what we had before moving to addWatchFile in rsc:css-virtual #847.
vite-plugin-react/packages/plugin-rsc/src/plugin.ts
Lines 2446 to 2449 in a7b26f5
This logic can be naturally subsumed there by adding collected to visitedFiles. Currently missing but it seems like that was an oversight.
There was a problem hiding this comment.
Oops, relying on addWatchFile broke the CI ba6c1c2
| // cssLinkPrecedence: false — without `precedence` React leaves the | ||
| // <link> as a regular DOM element (no auto-dedupe / resource handling) | ||
| // and Vite's client CSS HMR can't swap it by href-pathname match either | ||
| // (see hotUpdate above), so bake the HMR timestamp into the href and | ||
| // let the RSC-side Flight refetch drive the swap | ||
| // Default (true) leaves the href bare so Vite's client CSS HMR can swap | ||
| // the <link> in place — matches pre-fix behavior |
There was a problem hiding this comment.
So, we can drive css swap only for non float case, but the same technique won't be applicable for float case?
Based on my previous comment, it looks like float css would keep appending instead of swapping but not sure if I verified that properly and I might have confused with Vite side css behavior.
There was a problem hiding this comment.
it looks like float css would keep appending instead of swapping
Okay, I think I verified locally just in case. I feel like this means that we've been relying on somewhat not-so-well-defined behavior of float css not re-inserting the original css that get removed by vite.
Probably had the same conclusion when I was writing that comment, but just iterating for the record. Will probably polish the comment accordinglly.
cssLinkPrecedence: false
cssLinkPrecedence: falsecssLinkPrecedence: false
Co-authored-by: Codex <noreply@openai.com>
hi-ogawa
left a comment
There was a problem hiding this comment.
Thanks for PR and analysis! Merging.

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
rscVite 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
cssLinkPrecedence: falsee.g TanStack Start.createServerFn+renderServerComponent, etc.).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)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<link href>incollectCss. Only whencssLinkPrecedence: falseand the consumer isn't alreadyclient, append?t=<lastHMRTimestamp>. The default Float path stays bare-href so Vite's in-place<link>swap keeps workinghotUpdate. WhencssLinkPrecedence: falseand the changed CSS has no client-side JS importer, return[]. This is what prevents Vite'sPromise.allfrom 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