docs/oss/migrating/migrating-to-rsc.md
This guide covers the React-side challenges of migrating an existing React on Rails application to React Server Components (RSC). It focuses on how to restructure your component tree, handle Context and state management, migrate data fetching patterns, deal with third-party library compatibility, and avoid common pitfalls.
[!NOTE] Summary for AI agents: Use this page when the user has an existing React on Rails app and wants to adopt RSC. This covers the React-side migration (component restructuring, state, data fetching). For the initial RSC setup, see the RSC tutorial. RSC requires Pro with the Node renderer.
React on Rails Pro required: RSC support requires React on Rails Pro 4+ with the node renderer. The Pro gem provides the streaming view helpers (
stream_react_component,stream_react_component_with_async_props,rsc_payload_react_component, andrsc_payload_react_component_with_async_props), the RSC webpack plugin and loader, and theregisterServerComponentAPI. For setup, see the RSC tutorial. For upgrade steps, see the performance breakthroughs guide.
React Server Components offer significant performance benefits when used correctly:
Real-world results from teams that have adopted server-first rendering (RSC in production, plus DoorDash's pre-RSC SSR baseline):
Full case studies with context and caveats: Migration Success Stories.
However, these benefits require intentional architecture changes. Simply adding 'use client' everywhere preserves the status quo -- 'use client' is a boundary marker, not a component annotation. The guides below walk you through the restructuring needed to capture real gains.
This migration guide is organized as a series of focused articles. We recommend reading them in order, but each is self-contained:
How to set up the RSC infrastructure before migrating any components. Covers:
'use client' to all existing component entry points (so nothing changes yet)How to restructure your component tree for RSC. Covers:
'use client' to leaves)How to handle React Context and global state in an RSC world. Covers:
'use client' provider components)React.cache() as a server-side alternative to ContextHow to migrate from client-side data fetching to server component patterns. Covers:
useEffect + fetch with async Server Componentsuse() hook and SuspenseHow to keep HTTP response semantics in Rails while rendering the UI with RSC. Covers:
404 and not-found UI patternsHow to handle libraries that aren't RSC-compatible. Covers:
'use client' wrapper filesserver-only and client-only packagesHow to debug and avoid common problems. Covers:
How to optimize RSC Flight payload size for better performance. Covers:
How to prove that an RSC conversion improved the page users actually see. Covers:
How to render public/static-ish pages as mostly static RSC HTML while preserving a few browser behaviors without loading the full global application pack. Covers:
clientReferences caveatsBefore diving into the React patterns, understand how RSC maps to React on Rails' architecture.
Multiple component roots. Unlike single-page apps with one App.jsx root, React on Rails renders independent component trees from ERB views. Each react_component, stream_react_component, or stream_react_component_with_async_props call is a separate root. You migrate per-component, not per-app.
Three API changes per component. Each component you migrate touches three layers:
| Layer | Before | After |
|---|---|---|
| ERB view helper | react_component("Product", ...) | stream_react_component("Product", ...) or stream_react_component_with_async_props("Product", ...) when Rails emits async props |
| JS registration | ReactOnRails.register({ Product }) | registerServerComponent (signature varies per bundle — see details) |
| Controller | Standard Rails controller | Add include ReactOnRailsPro::Stream |
Three webpack bundles. RSC requires separate client, server, and RSC bundles. The registerServerComponent API behaves differently in each:
Setup instructions: For webpack configuration, bundle structure, route setup, and step-by-step instructions, see the React on Rails Pro RSC tutorial. This guide focuses on the React-side patterns you'll need after setup is complete.
Tailored for React on Rails' multi-root architecture:
'use client' to all component entry points, and switch to streaming rendering. The app works identically -- nothing changes yet.'use client' from the root component to its interactive children, letting parent components become Server Components.stream_react_component for streaming SSR, stream_react_component_with_async_props for progressively emitted Rails data, and server-side data fetching.This approach lets you migrate incrementally, one component at a time, without ever breaking your app.
Before you start, audit your components using this classification:
| Category | Criteria | Action |
|---|---|---|
| Server-ready (green) | No hooks, no browser APIs, no event handlers | Remove 'use client' -- these are Server Components by default |
| Refactorable (yellow) | Mix of data fetching and interactivity | Split into a Server Component (data) + Client Component (interaction) |
| Client-only (red) | Uses useState, useEffect, event handlers, browser APIs | Keep 'use client' -- these remain Client Components |
Before starting any component migration, verify these items. Skipping them is the most common source of wasted debugging time:
react and react-dom on 19.2.x with patch >= 19.2.7, with matching versions (yarn why react shows no duplicates)NodeRenderer, not ExecJS. If config.server_renderer is not set to "NodeRenderer", migrate firstreact-on-rails-rsc 19.2.x with patch >= 19.2.1 -- install 19.2.1 or later on the 19.2.x line; check with yarn why react-on-rails-rscreact-client-manifest.json and react-server-client-manifest.json exist in your webpack output directoryrsc_payload_route in config/routes.rbHMR=true RSC_BUNDLE_ONLY=true bin/shakapacker --watch)These mistakes account for the majority of setup failures:
| Mistake | Symptom | Fix |
|---|---|---|
Missing rsc_payload_route in routes | 404 on RSC payload requests | Add rsc_payload_route to config/routes.rb |
| Only 2 webpack bundles (forgot RSC) | Components remain Client Components after removing 'use client' | Create rscWebpackConfig.js and add to build pipeline (Step 4) |
'use client' on bundle entry files instead of component files | Can't migrate components individually | Move 'use client' to each component source file (Step 5) |
Missing 'use client' on .server.jsx files | Auto-bundled components break after enabling RSC | .server.jsx is a bundle convention, not an RSC designation -- add 'use client' to both .client.jsx and .server.jsx |
React version duplicates in node_modules | Cryptic hook errors, "Invalid hook call" | Deduplicate with yarn why react and webpack aliases |
Not switching to stream_react_component | No streaming benefits, components render synchronously | Replace react_component with stream_react_component in views |
Missing include ReactOnRailsPro::Stream in controller | stream_view_containing_react_components undefined | Add the concern to controllers that render React components |
react_component_hash with React 19's built-in <title>, <meta>, and <link> hoisting. Native metadata works with streaming and RSC out of the box.404, redirects, and cache policy in Rails while rendering route UI with RSC.