Skip to content

[mirror] Pro RSC migration 3/3: React Server Components demo on webpack#1

Open
yashwant86 wants to merge 20 commits intomm-base-729from
mm-pr-729
Open

[mirror] Pro RSC migration 3/3: React Server Components demo on webpack#1
yashwant86 wants to merge 20 commits intomm-base-729from
mm-pr-729

Conversation

@yashwant86
Copy link
Copy Markdown

@yashwant86 yashwant86 commented Apr 26, 2026

Mirror of upstream shakacode#729 (open PR live findings) for benchmark. Do not merge.


Summary by MergeMonkey

  • Fresh Additions:
    • React Server Components (RSC) demo page with live activity refresh, server environment info, and streamed comments feed.
    • RSC webpack configuration and payload generation endpoint for server-side component rendering.
    • Navigation link to RSC demo page in main navigation bar.
  • Infrastructure:
    • Added 'use client' directives to existing client-side components for RSC compatibility.
    • Updated Rails controller to scope before_action and add RSC demo route.
    • Configured Node.js renderer to support RSC async operations and console logging.

ihabadham and others added 20 commits April 23, 2026 20:56
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>
@github-actions
Copy link
Copy Markdown

🚀 Quick Review App Commands

Welcome! Here are the commands you can use in this PR:

/deploy-review-app

Deploy your PR branch for testing

/delete-review-app

Remove the review app when done

/help

Show detailed instructions, environment setup, and configuration options.


@bot-mergemonkey
Copy link
Copy Markdown

bot-mergemonkey Bot commented Apr 26, 2026

Risk AssessmentNEEDS-TESTING · ~45 min review

Focus areas: RSC webpack loader injection and 'use client' directive detection · Markdown parsing and HTML sanitization in CommentsFeed · RSCRoute error boundary and refetchComponent integration · Node.js renderer stubTimers and async logging configuration

Assessment: Adds RSC demo with server-side markdown parsing, sanitization, and async streaming — requires integration testing.

Walkthrough

User navigates to /server-components route. Rails controller fetches last 10 comments and renders ServerComponentsPage via stream_react_component with prerender:true. Webpack RSC bundle detects 'use client' directives and separates server/client components. ServerComponentsPage (server component) renders ServerInfo (Node.js os module), TogglePanel (client component wrapping server content), and CommentsFeed (async server component with markdown parsing). LiveActivityRefresher (client) uses RSCRoute to fetch LiveActivity server component on demand, with ErrorBoundary catching failures. All server-only dependencies (lodash, marked, sanitize-html, os module) stay server-side; only client components ship JavaScript.

Changes

Files Summary
RSC Webpack Configuration
config/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 Integration
config/webpack/clientWebpackConfig.js
config/initializers/react_on_rails_pro.rb
config/routes.rb
renderer/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 & Components
app/controllers/pages_controller.rb
app/views/pages/server_components.html.erb
client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx
client/app/bundles/server-components/ror_components/LiveActivity.jsx
client/app/bundles/server-components/components/CommentsFeed.jsx
client/app/bundles/server-components/components/ServerInfo.jsx
client/app/bundles/server-components/components/TogglePanel.jsx
client/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' Directives
client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx
client/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsx
client/app/bundles/comments/startup/App/ror_components/App.jsx
client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx
client/app/packs/stimulus-bundle.js
client/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 & Routes
client/app/bundles/comments/constants/paths.js
client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx
Procfile.dev
Adds SERVER_COMPONENTS_PATH constant, RSC demo navigation link, and wp-rsc webpack watcher process for RSC bundle development.
Dependencies & Tests
package.json
spec/requests/server_components_spec.rb
spec/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
Loading

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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bot-mergemonkey
Copy link
Copy Markdown

Actionable Comments Posted: 1

🧾 Coverage Summary
✔️ Covered (29 files)
- Procfile.dev
- app/controllers/pages_controller.rb
- app/views/pages/server_components.html.erb
- client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
- client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx
- client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx
- client/app/bundles/comments/constants/paths.js
- client/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsx
- client/app/bundles/comments/startup/App/ror_components/App.jsx
- client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx
- client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx
- client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx
- client/app/bundles/server-components/components/CommentsFeed.jsx
- client/app/bundles/server-components/components/LiveActivityRefresher.jsx
- client/app/bundles/server-components/components/ServerInfo.jsx
- client/app/bundles/server-components/components/TogglePanel.jsx
- client/app/bundles/server-components/ror_components/LiveActivity.jsx
- client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx
- client/app/packs/stimulus-bundle.js
- client/app/packs/stores-registration.js
- config/initializers/react_on_rails_pro.rb
- config/routes.rb
- config/webpack/clientWebpackConfig.js
- config/webpack/rscWebpackConfig.js
- config/webpack/webpackConfig.js
- package.json
- renderer/node-renderer.js
- spec/requests/server_components_spec.rb
- spec/system/server_components_demo_spec.rb

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