Skip to content

RSC: CSS imported behind 'use client' flickers/FOUCs because client manifest tracks only JS, not CSS #3211

@AbanoubGhadban

Description

@AbanoubGhadban

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)

  1. 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
    
  2. 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.
  3. 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions