[mirror] Pro RSC migration 3/3: React Server Components demo on webpack#1
[mirror] Pro RSC migration 3/3: React Server Components demo on webpack#1yashwant86 wants to merge 20 commits intomm-base-729from
Conversation
Add the three RSC fields per the marketplace demo initializer (react-on-rails-demo-marketplace-rsc/config/initializers/ react_on_rails_pro.rb): - enable_rsc_support = true - rsc_bundle_js_file = "rsc-bundle.js" - rsc_payload_generation_url_path = "rsc_payload/" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RSCWebpackPlugin({ isServer: false }) on the client bundle scans for
'use client' files and adds them as entry points so they appear in the
client manifest (react-client-manifest.json). Without this, client
components referenced in RSC payloads wouldn't have matching chunks
in the client bundle.
clientReferences scoped to config.source_path, consistent with the
server bundle's scoping in serverWebpackConfig.js.
Reference: Pro dummy clientWebpackConfig.js:16-24.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Derives from serverWebpackConfig(true) — inherits target:'node', libraryTarget:'commonjs2', CSS filtering, and all server transforms. Adds three RSC-specific pieces: 1. RSC WebpackLoader pushed into the babel rule's use array (runs before babel per right-to-left order) to detect 'use client' directives in raw source and replace client exports with registerClientReference proxies. 2. react-server resolve condition so React's RSC-specific entry points are used. 3. react-dom/server aliased to false (RSC bundles generate Flight payloads, not HTML; importing react-dom/server causes a runtime error). Loader placement follows Pro dummy pattern (push into rule.use) per docs/oss/migrating/rsc-preparing-app.md:167-195. NOT marketplace's enforce:'post' which runs after transpilation and can miss directive AST nodes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add RSC_BUNDLE_ONLY env gate alongside the existing SERVER_BUNDLE_ONLY and CLIENT_BUNDLE_ONLY gates. Procfile.dev will use RSC_BUNDLE_ONLY=yes bin/shakapacker --watch to build the RSC bundle separately during development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RSC auto-classification: files without 'use client' are registered as Server Components via registerServerComponent(). All existing components are Client Components (hooks, Redux, Router, event handlers), so they need the directive to preserve current behavior. Entry points (7 ror_components/ files): - App.jsx, NavigationBarApp.jsx, RouterApp.client.jsx, RouterApp.server.jsx (SSR wrapper, NOT a Server Component), SimpleCommentScreen.jsx, Footer.jsx, RescriptShow.jsx Pack entry files (2): - stores-registration.js, stimulus-bundle.js Per docs/oss/migrating/rsc-preparing-app.md Step 5 and docs/pro/react-server-components/create-without-ssr.md:52. Matches Justin's PR 723 final state exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- rsc_payload_route in routes.rb enables the Flight protocol endpoint for client-side RSC payload fetching. - get "server-components" route maps to pages#server_components. - View uses prerender: false (RSC components are streamed via the payload route, not traditional SSR prerender) and auto_load_bundle: false (ServerComponentsPage is not in ror_components/, so auto-discovery doesn't find its pack). - trace: Rails.env.development? gates server-timing headers to dev. Reference: Justin's PR 723 commits 4d09e13 + 0d8d75a. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server Components (no 'use client'):
- ServerComponentsPage.jsx: demo container showing RSC streaming
- components/ServerInfo.jsx: displays server environment info
- components/CommentsFeed.jsx: async data fetch with timeout,
env-gated delay, img sanitization, data.comments unwrap
Client Component ('use client'):
- components/TogglePanel.jsx: interactive panel demonstrating
'use client' boundary within a server component tree
Salvaged from Justin's PR 723 final state per the journey report
KEEP table. CommentsFeed specifically from commit f008295
(has the fetch timeout + sanitization fixes from review).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Procfile.dev: wp-rsc process runs RSC_BUNDLE_ONLY=yes bin/shakapacker --watch alongside existing client, server, and renderer processes. - paths.js: SERVER_COMPONENTS_PATH constant. - NavigationBar.jsx: "RSC Demo" link in the nav bar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default branch (no env vars) runs during bin/shakapacker for production/CI builds. Without the RSC config in the array, the RSC bundle only gets built when RSC_BUNDLE_ONLY is set (dev watchers). Production deploys + CI would miss it. The *_BUNDLE_ONLY gates remain for dev Procfile processes (each watcher builds one bundle in isolation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Justin's PR used manual registerServerComponent() in stimulus-bundle.js because his custom rspackRscPlugin didn't integrate with the auto-bundling flow. With the upstream RSCWebpackPlugin, auto-bundling works: the generate_packs task scans ror_components/ directories, classifies files without 'use client' as Server Components, and generates the registration file in generated/ServerComponentsPage.js automatically. Moved from: bundles/server-components/ServerComponentsPage.jsx Moved to: bundles/server-components/ror_components/ServerComponentsPage.jsx Updated relative imports (./components/ -> ../components/) and flipped the view from auto_load_bundle: false to true. No manual registration, no stimulus-bundle.js modification. Matches the Pro dummy pattern where server component sources sit in the auto-discovered directory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous implementation only handled Array.isArray(rule.use) and only looked for babel-loader. The tutorial uses swc as its transpiler (shakapacker.yml: javascript_transpiler: swc), which makes Shakapacker generate rule.use as a FUNCTION, not an array. The RSC loader was therefore never attached to the transpilation rule — 'use client' files were left untransformed in the RSC bundle, producing 134 webpack warnings (export 'useState' not found in 'react' etc.) and setting up a runtime error when the RSC renderer would try to call client components directly instead of emitting client references. Follow the pattern from docs/oss/migrating/rsc-preparing-app.md:167 verbatim, which handles both forms and both loader names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The renderer's stubTimers default of true replaces setTimeout with a no-op inside the VM. React's RSC server renderer uses setTimeout internally for Flight-protocol yielding, so stubbing it makes the RSC stream open without ever emitting a chunk. The request reaches the worker, the worker holds the accepted socket, but no data flows. Fastify eventually closes the idle connection at keepAliveTimeout (~72s), HTTPX retries once by its retries plugin, and Rails sees HTTPX::Connection::HTTP2::GoawayError after ~144s. Non-RSC SSR is unaffected because it doesn't rely on setTimeout for its async scheduling — only RSC's streaming path hits this. Verified by running a second renderer alongside on another port with RENDERER_STUB_TIMERS=false: the stuck path returned a full 9.7KB RSC payload for ServerComponentsPage in 422ms, vs. the default renderer timing out on the same request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this flag, console.* calls made inside async Server Components are captured by the renderer's per-request sharedConsoleHistory but not replayed back to Rails' logs. Any error-path logging from an async component (for example, a catch block that console.errors before returning an error fallback div) disappears, making runtime failures invisible. The generator template, RORP spec dummy, and every maintained RSC demo set this to true for the same reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop self-fetch (Server Component → fetch → Rails) anti-pattern that docs/oss/migrating/rsc-data-fetching.md and rsc-troubleshooting.md explicitly warn against (circular request: Node renderer → Rails → Node renderer). Move comments loading into the Rails controller and pass them as props through stream_react_component, matching every maintained RSC demo and the dummy app. Changes: - PagesController#server_components now loads @server_components_comments from the Rails DB and renders via stream_view_containing_react_components. Includes ReactOnRailsPro::Stream (provides the helper + ActionController::Live). - View switches react_component → stream_react_component, passes comments as props. - ServerComponentsPage receives comments and forwards to CommentsFeed. - CommentsFeed becomes a pure-render Server Component: receives comments as a prop, drops fetch / AbortController / RAILS_INTERNAL_URL handling / try-catch error fallback / lodash. Keeps marked + sanitize-html for server-side markdown rendering. Suspense boundary stays; the demo delay (default-on, RSC_SUSPENSE_DEMO_DELAY=false to disable) gives Suspense visible work to wait on. - ServerInfo: drop unused async keyword. - No PropTypes added (React 19 removed propTypes runtime validation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demonstrates two RSC capabilities the demo previously didn't cover: 1. Client-fetched server components via `RSCRoute` — a new section "Live Server Activity" shows server-side data (timestamp, free RAM, uptime). A Refresh button bumps the props' refreshKey, causing `RSCRoute` to fetch a fresh RSC payload over HTTP. No client-side fetcher code, no JSON parsing, no loading-state plumbing — the server re-renders, streams the result back, and React reconciles. 2. Error handling per the docs' canonical pattern (`rsc-troubleshooting.md` "Error Boundary Limitations"): a Simulate Error button forces the server component to throw. The error surfaces on the client as `ServerComponentFetchError`, is caught by `<ErrorBoundary>` from `react-error-boundary`, and the fallback UI exposes a Retry button that re-fetches with corrected props. `LiveActivity.jsx` lives in `ror_components/` so auto_load_bundle registers it as a Server Component reachable by `RSCRoute` via name. `LiveActivityRefresher.jsx` is a single-file Client Component (no `.client/.server` wrapper pair needed) — registerServerComponent on the outer `ServerComponentsPage` provides the RSC context for `RSCRoute` calls anywhere in its subtree. Also restores `updated_at` to the comments prop set so the controller matches the canonical `_comment.json.jbuilder` partial's field shape (reverting a UI-driven minimization that coupled the resource representation to today's component code). Adds: `react-error-boundary@^4.1.2`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit's retry path used resetErrorBoundary + onReset state
mutation alone — RSCRoute would auto-fetch on the post-reset render
because props had changed. Functional, but didn't actually demonstrate
useRSC().refetchComponent, which is the canonical RoR Pro RSC retry
API per rsc-troubleshooting.md.
Switch to: refetchComponent('LiveActivity', correctedProps) primes the
cache, then resetErrorBoundary fires; the post-reset render hits cache
instead of triggering a second fetch. This shows the explicit API call
the docs recommend, while still handling our intentional-error case
(refetching with simulateError=false instead of the original throwing
props).
Page description updated to mention refetchComponent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pages_controller.rb: scope `before_action :set_comments` to actions that use @comments (index, no_router) — server_components/simple/rescript no longer trigger an unused query. - server_components.html.erb: drop hardcoded `id:` from stream_react_component call. RoR auto-generates stable unique IDs; hardcoding risks DOM-id collisions if the helper is ever called twice and diverges from the default pattern used by /no_router and /simple. - CommentsFeed.jsx: flip RSC_SUSPENSE_DEMO_DELAY to opt-in (=== 'true'). The previous opt-out guard (!== 'false') made the 800ms delay fire by default in any deploy without the env var explicitly set. app.yml: add RSC_SUSPENSE_DEMO_DELAY=true to the review-app template so the demo still shows the streaming fallback visibly. - ServerInfo.jsx: drop the Hostname row. The K8s pod name (e.g. rails-5fc66bddf6-r8b5r) is infrastructure-fingerprinting fodder for forks deployed publicly, and ServerInfo's "look, server-side data" intent is fully covered by the remaining six fields. - stores-registration.js: add a one-line comment noting why 'use client' sits on a webpack pack entry (excludes the registration code's dependency graph from the RSC bundle). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Request spec mirrors the dummy's `requests/rsc_payload_spec.rb` pattern (NDJSON parsing, html-chunk presence). Covers: - GET /server-components returns the demo page shell - /rsc_payload/ServerComponentsPage streams a valid NDJSON payload - /rsc_payload/LiveActivity streams a valid NDJSON payload System spec covers the user-facing behaviors of the new sections: - Page renders the four demo section headings - ServerInfo labels appear (Platform, Architecture, Node.js, CPU Cores) - Live Activity shows the live stats labels + initial Refresh count - Refresh button increments the counter (RSCRoute fetch happens) - Simulate Error → ErrorBoundary fallback → Retry recovers Tests behaviors only — no internal state assertions, no specific values (server time, RAM number) that would be flaky. Uses existing rails_helper infrastructure (headless Chrome via DriverRegistration, Capybara defaults). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The setTimeout was firing on every request, including production deployments — verified via curl on the review-app: /rsc_payload/ LiveActivity took ~1000ms vs ~620ms for /rsc_payload/ServerComponentsPage across 3 trials, ~400ms gap matching the unconditional 300ms delay. Same opt-in gate as CommentsFeed: enabled when env var is exactly 'true', off by default. Review-app keeps the visible behavior because app.yml sets RSC_SUSPENSE_DEMO_DELAY=true. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a local Suspense boundary, RSCRoute's in-flight fetch suspension bubbles up to whatever outer Suspense the Pro stream_react_component infrastructure provides. That outer fallback wipes the entire rendered tree, so during a Refresh or Simulate Error click, the whole page collapsed to viewport height for ~500ms and the browser snapped scrollY to 0 (since the page wasn't tall enough to preserve the prior scroll position). Verified empirically via window.scrollY + document.body.scrollHeight sampling on the deployed review-app: pre-fix, pageHeight went from 2193px to 764px between +50ms and +500ms after click. Local Suspense boundary contains the suspension to the LiveActivity section; same- shape skeleton fallback keeps the section's height stable so the surrounding layout doesn't shift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🚀 Quick Review App CommandsWelcome! Here are the commands you can use in this PR:
|
⚡ Risk Assessment —
|
| Files | Summary |
|---|---|
RSC Webpack Configurationconfig/webpack/rscWebpackConfig.js, webpackConfig.js |
Adds RSC-specific webpack configuration that detects 'use client' directives, injects RSCWebpackPlugin, and conditionally builds RSC bundle alongside client/server bundles based on RSC_BUNDLE_ONLY environment variable. |
RSC Client Integrationconfig/webpack/clientWebpackConfig.jsconfig/initializers/react_on_rails_pro.rbconfig/routes.rbrenderer/node-renderer.js |
Enables RSC support in Rails initializer, adds rsc_payload route, configures Node.js renderer to support async RSC operations and console logging, and injects RSCWebpackPlugin into client webpack config. |
RSC Demo Page & Componentsapp/controllers/pages_controller.rbapp/views/pages/server_components.html.erbclient/app/bundles/server-components/ror_components/ServerComponentsPage.jsxclient/app/bundles/server-components/ror_components/LiveActivity.jsxclient/app/bundles/server-components/components/CommentsFeed.jsxclient/app/bundles/server-components/components/ServerInfo.jsxclient/app/bundles/server-components/components/TogglePanel.jsxclient/app/bundles/server-components/components/LiveActivityRefresher.jsx |
Implements RSC demo page with server environment info (Node.js os module), streamed comments feed with markdown parsing, live activity refresh via RSCRoute, and interactive toggle demonstrating server/client component mixing. |
Client Component 'use client' Directivesclient/app/bundles/comments/components/Footer/ror_components/Footer.jsxclient/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsxclient/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsxclient/app/bundles/comments/startup/App/ror_components/App.jsxclient/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsxclient/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsxclient/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsxclient/app/packs/stimulus-bundle.jsclient/app/packs/stores-registration.js |
Adds 'use client' directives to mark components as client-side for RSC webpack loader, ensuring they are excluded from RSC bundle and properly hydrated on client. |
Navigation & Routesclient/app/bundles/comments/constants/paths.jsclient/app/bundles/comments/components/NavigationBar/NavigationBar.jsxProcfile.dev |
Adds SERVER_COMPONENTS_PATH constant, RSC demo navigation link, and wp-rsc webpack watcher process for RSC bundle development. |
Dependencies & Testspackage.jsonspec/requests/server_components_spec.rbspec/system/server_components_demo_spec.rb |
Adds react-error-boundary dependency, request tests for RSC payload endpoint validation, and system tests for demo page rendering and interactivity. |
Sequence Diagram
sequenceDiagram
participant Browser
participant Rails
participant RSCRenderer
participant Webpack
Browser->>Rails: GET /server-components
Rails->>Rails: fetch comments, set @server_components_comments
Rails->>Browser: render ServerComponentsPage (prerender:true)
Browser->>RSCRenderer: stream_react_component triggers RSC render
RSCRenderer->>Webpack: resolve ServerComponentsPage (server component)
Webpack->>RSCRenderer: ServerInfo, CommentsFeed (server), TogglePanel (client wrapper)
RSCRenderer->>RSCRenderer: execute ServerInfo (os.platform, os.freemem)
RSCRenderer->>RSCRenderer: execute CommentsFeed (marked.parse, sanitize-html)
RSCRenderer->>Browser: stream RSC payload (HTML chunks)
Browser->>Browser: hydrate TogglePanel, LiveActivityRefresher (client components)
Browser->>Browser: render static ServerInfo, CommentsFeed HTML
Browser->>Rails: click Refresh → POST /rsc_payload/LiveActivity
Rails->>RSCRenderer: render LiveActivity with new props
RSCRenderer->>RSCRenderer: fetch os.freemem, os.uptime
RSCRenderer->>Browser: stream updated RSC payload
Browser->>Browser: ErrorBoundary catches error if simulateError=true
Browser->>Browser: click Retry → refetchComponent with corrected props
Dig Deeper With Commands
/review <file-path> <function-optional>/chat <file-path> "<question>"/roast <file-path>
Runs only when explicitly triggered.
|
|
||
| describe "Live Server Activity (RSCRoute)" do | ||
| it "shows the initial activity card with the live stats labels" do | ||
| expect(page).to have_content("SERVER TIME") |
There was a problem hiding this comment.
System spec asserts uppercase labels that aren't in the DOM text
The spec checks have_content('SERVER TIME'), 'FREE RAM', and 'UPTIME (HRS)', but LiveActivity.jsx renders the literals "Server Time", "Free RAM", and "Uptime (hrs)" and only uppercases them visually via the Tailwind uppercase class. Capybara's have_content matches against textContent, not computed CSS, so these assertions (and the same one on line 40) will fail.
Either change the spec to match the actual JSX text (e.g. have_content('Server Time')) or change the JSX literals to uppercase. Note have_content ignores CSS text-transform.
Actionable Comments Posted: 1🧾 Coverage Summary✔️ Covered (29 files) |
Mirror of upstream shakacode#729 (open PR live findings) for benchmark. Do not merge.
Summary by MergeMonkey