Summary
When a true React Server Component renders a 'use client' boundary, any CSS imported by that boundary (or its descendants) is not preloaded by react-on-rails-rsc / react-on-rails-pro. It loads only as a side-effect of the JS chunk evaluating, several hundred ms to several seconds after first paint. This produces a visible flash of unstyled content.
The bug is invisible in react_on_rails_pro/spec/dummy because none of its RSC components import CSS — they all use inline style={{ … }}. Real apps converting their page shell (Layout, Header, Footer, etc.) behind 'use client' will hit this on the first conversion.
Reproduction
Real-app branch (Hichee, converting FaqPage to true RSC) with:
enable_rsc_support = true
react_component("FaqPage", prerender: false) in the view
FaqPageProviders.jsx marked 'use client', importing Layout, IntlProviderWrapper, ScreenSizeContext, AdBlockContext (which transitively import *.module.scss)
Symptom: page renders unstyled at first paint, then snaps to styled ~1–3s later (network-bound).
Root cause (verified by reading sources)
-
RSCWebpackPlugin (the patched react-server-dom-webpack-plugin.js shipped by react-on-rails-rsc) records JS files only. The chunk-collection loop pushes only entries where file.endsWith('.js'). The CSS sibling produced by mini-css-extract-plugin for the same chunk group is dropped on the floor.
Evidence — output react-client-manifest.json for FaqPageProviders.jsx:
chunks: 20 entries, 100% .bundle.js, 0 .css
-
react-on-rails-pro's RSC renderer never emits any CSS metadata. Across packages/react-on-rails-pro/src/, grep returns zero matches for preinitStyle, preloadStyle, <link rel=\"stylesheet\">, precedence, or entryCSSFiles. Specifically:
streamServerRenderedReactComponent.ts:78–100 calls renderToPipeableStream and pipes through injectRSCPayload — no CSS handling.
registerServerComponent/client.tsx, RSCRoute.tsx, getReactServerComponent.client.ts — no CSS handling.
-
auto_load_bundle only sees CSS imported by the page's static entry. With true RSC, the auto-generated entry collapses to:
import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client';
registerServerComponent(\"FaqPage\");
So manifest.json → entrypoints['generated/FaqPage'].assets.css = ['vendor.css'] only. All Layout/Header/Footer/Alerts CSS lives behind the 'use client' boundary and is absent from the head.
Net effect: the only path by which CSS reaches the page is the mini-css-extract-plugin runtime appending a <link> when each JS chunk evaluates — which happens after RSC payload fetch + JS chunk load. Hence the flicker.
Why existing RoR Pro RSC users haven't hit this
They aren't doing true RSC. The current pattern ('use client' at the page entry, e.g. AccountsEdit.client.jsx) keeps the auto-generated pack as import Component; ReactOnRails.register({Component}), so webpack walks the static import graph and the page entrypoint manifest correctly lists every .css file. That's not RSC — it's a fully-client component routed through RSC infrastructure.
The first user converting an actual page shell to true RSC hits this immediately.
Proposed fix
react-server-dom-webpack ships the primitives (preinitStyle, preloadStyle) but calling them is the framework's job. We need to:
- Plugin — in
react-on-rails-rsc's react-server-dom-webpack-plugin.js, in the same chunk-iteration loop the recent +19.0.5-rc.1 patch fixed, also collect .css files into the per-module record (either interleaved into chunks or as a sibling cssChunks).
- Renderer (SSR path) — in
streamServerRenderedReactComponent.ts, for every client reference encountered, emit <link rel=\"stylesheet\" precedence=\"ror-rsc\" href={cssHref}/> and call ReactDOM.preloadStyle(href) for each CSS chunk before the client component. React 19 hoists these into <head>, dedupes, and blocks tree commit on stylesheet load — making FOUC structurally impossible.
- Renderer (CSR path,
prerender: false) — in RSCRoute.tsx / getReactServerComponent.client.ts, when decoding a client reference, also call ReactDOM.preinitStyle(href, { precedence: 'ror-rsc' }) for each CSS chunk so React hoists <link> into <head> and blocks commit on stylesheet load. This is required for RoR's prerender: false flow where RSC is rendered purely on the client.
- Test — add a CSS import to
spec/dummy/client/app/components/RSCPostsPage/Main.jsx (e.g. a SCSS module) and a Playwright assertion that the rendered element has its styled background color on first paint. This regression-traps the bug.
References
- Real-app artifact reproducing the bug:
client/patches/react-on-rails-rsc+19.0.5-rc.1.patch — fixes the JS-discovery loop but does not extend it to CSS.
Summary
When a true React Server Component renders a
'use client'boundary, any CSS imported by that boundary (or its descendants) is not preloaded byreact-on-rails-rsc/react-on-rails-pro. It loads only as a side-effect of the JS chunk evaluating, several hundred ms to several seconds after first paint. This produces a visible flash of unstyled content.The bug is invisible in
react_on_rails_pro/spec/dummybecause none of its RSC components import CSS — they all use inlinestyle={{ … }}. Real apps converting their page shell (Layout, Header, Footer, etc.) behind'use client'will hit this on the first conversion.Reproduction
Real-app branch (Hichee, converting
FaqPageto true RSC) with:enable_rsc_support = truereact_component("FaqPage", prerender: false)in the viewFaqPageProviders.jsxmarked'use client', importingLayout,IntlProviderWrapper,ScreenSizeContext,AdBlockContext(which transitively import*.module.scss)Symptom: page renders unstyled at first paint, then snaps to styled ~1–3s later (network-bound).
Root cause (verified by reading sources)
RSCWebpackPlugin(the patchedreact-server-dom-webpack-plugin.jsshipped byreact-on-rails-rsc) records JS files only. The chunk-collection loop pushes only entries wherefile.endsWith('.js'). The CSS sibling produced bymini-css-extract-pluginfor the same chunk group is dropped on the floor.Evidence — output
react-client-manifest.jsonforFaqPageProviders.jsx:react-on-rails-pro's RSC renderer never emits any CSS metadata. Acrosspackages/react-on-rails-pro/src/,grepreturns zero matches forpreinitStyle,preloadStyle,<link rel=\"stylesheet\">,precedence, orentryCSSFiles. Specifically:streamServerRenderedReactComponent.ts:78–100callsrenderToPipeableStreamand pipes throughinjectRSCPayload— no CSS handling.registerServerComponent/client.tsx,RSCRoute.tsx,getReactServerComponent.client.ts— no CSS handling.auto_load_bundleonly sees CSS imported by the page's static entry. With true RSC, the auto-generated entry collapses to:So
manifest.json → entrypoints['generated/FaqPage'].assets.css = ['vendor.css']only. All Layout/Header/Footer/Alerts CSS lives behind the'use client'boundary and is absent from the head.Net effect: the only path by which CSS reaches the page is the
mini-css-extract-pluginruntime appending a<link>when each JS chunk evaluates — which happens after RSC payload fetch + JS chunk load. Hence the flicker.Why existing RoR Pro RSC users haven't hit this
They aren't doing true RSC. The current pattern (
'use client'at the page entry, e.g.AccountsEdit.client.jsx) keeps the auto-generated pack asimport Component; ReactOnRails.register({Component}), so webpack walks the static import graph and the page entrypoint manifest correctly lists every.cssfile. That's not RSC — it's a fully-client component routed through RSC infrastructure.The first user converting an actual page shell to true RSC hits this immediately.
Proposed fix
react-server-dom-webpackships the primitives (preinitStyle,preloadStyle) but calling them is the framework's job. We need to:react-on-rails-rsc'sreact-server-dom-webpack-plugin.js, in the same chunk-iteration loop the recent+19.0.5-rc.1patch fixed, also collect.cssfiles into the per-module record (either interleaved intochunksor as a siblingcssChunks).streamServerRenderedReactComponent.ts, for every client reference encountered, emit<link rel=\"stylesheet\" precedence=\"ror-rsc\" href={cssHref}/>and callReactDOM.preloadStyle(href)for each CSS chunk before the client component. React 19 hoists these into<head>, dedupes, and blocks tree commit on stylesheet load — making FOUC structurally impossible.prerender: false) — inRSCRoute.tsx/getReactServerComponent.client.ts, when decoding a client reference, also callReactDOM.preinitStyle(href, { precedence: 'ror-rsc' })for each CSS chunk so React hoists<link>into<head>and blocks commit on stylesheet load. This is required for RoR'sprerender: falseflow where RSC is rendered purely on the client.spec/dummy/client/app/components/RSCPostsPage/Main.jsx(e.g. a SCSS module) and a Playwright assertion that the rendered element has its styled background color on first paint. This regression-traps the bug.References
client/patches/react-on-rails-rsc+19.0.5-rc.1.patch— fixes the JS-discovery loop but does not extend it to CSS.