Skip to main content

Migrating Your React App to React Server Components

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.

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, rsc_payload_react_component), the RSC webpack plugin and loader, and the registerServerComponent API. For setup, see the RSC tutorial. For upgrade steps, see the performance breakthroughs guide.

Why Migrate?

React Server Components offer significant performance benefits when used correctly:

  • Significant reductions in client-side bundle size reported across RSC adoption case studies
  • Improvements in Google Speed Index and Total Blocking Time
  • Server-only dependencies (date-fns, marked, sanitize-html) never ship to the client

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.

Article Series

This migration guide is organized as a series of focused articles. We recommend reading them in order, but each is self-contained:

1. Preparing Your App

How to set up the RSC infrastructure before migrating any components. Covers:

  • Installing dependencies and configuring Rails for RSC
  • Creating the RSC webpack bundle and adding the RSC plugin to existing bundles
  • Adding 'use client' to all existing component entry points (so nothing changes yet)
  • Switching to streaming view helpers and controllers
  • After this step, the app works identically -- you're just ready for migration

2. Component Tree Restructuring Patterns

How to restructure your component tree for RSC. Covers:

  • The top-down migration strategy (start at layouts, push 'use client' to leaves)
  • The "donut pattern" for wrapping server content in client interactivity
  • Splitting mixed components into server and client parts
  • Passing Server Components as children to Client Components
  • Before/after examples of common restructuring patterns

3. Context, Providers, and State Management

How to handle React Context and global state in an RSC world. Covers:

  • Why Context doesn't work in Server Components and what to do about it
  • The provider wrapper pattern (creating 'use client' provider components)
  • Composing multiple providers without "provider hell"
  • Migrating Redux to work alongside RSC
  • Using React.cache() as a server-side alternative to Context
  • Theme, auth, and i18n provider patterns

4. Data Fetching Migration

How to migrate from client-side data fetching to server component patterns. Covers:

  • Replacing useEffect + fetch with async Server Components
  • Migrating from React Query / TanStack Query (prefetch + hydrate pattern)
  • Migrating from SWR (fallback data pattern)
  • Avoiding server-side waterfalls with parallel fetching
  • Streaming data with the use() hook and Suspense
  • When to keep client-side data fetching

5. Third-Party Library Compatibility

How to handle libraries that aren't RSC-compatible. Covers:

  • Creating thin 'use client' wrapper files
  • CSS-in-JS migration (styled-components, Emotion alternatives)
  • UI library compatibility (MUI, Chakra, Radix, shadcn/ui)
  • Form, animation, charting, and date library status
  • The barrel file problem and direct imports
  • Using server-only and client-only packages

6. Troubleshooting and Common Pitfalls

How to debug and avoid common problems. Covers:

  • Serialization boundary issues (what can cross server-to-client)
  • Import chain contamination and accidental client components
  • Hydration mismatch debugging
  • Error boundary limitations with RSC
  • Testing strategies (unit, integration, E2E)
  • TypeScript considerations
  • Performance monitoring and bundle analysis tools
  • Common error messages and their solutions

7. Flight Payload Optimization

How to optimize RSC Flight payload size for better performance. Covers:

  • What's in the Flight payload and why it can be surprisingly large
  • Why "all display-only = server" is an oversimplification
  • The counterintuitive pattern: when presentational Client Components outperform Server Components
  • How to measure and analyze your Flight payload
  • Compression effectiveness and the LCP tradeoff
  • React on Rails double JSON.stringify overhead

How RSC Maps to React on Rails

Before 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 or stream_react_component call is a separate root. You migrate per-component, not per-app.

Three API changes per component. Each component you migrate touches three layers:

LayerBeforeAfter
ERB view helperreact_component("Product", ...)stream_react_component("Product", ...)
JS registrationReactOnRails.register({ Product })registerServerComponent({ Product }) (in all three bundles)
ControllerStandard Rails controllerAdd include ReactOnRailsPro::Stream

Three webpack bundles. RSC requires separate client, server, and RSC bundles. The registerServerComponent API behaves differently in each:

  • RSC bundle -- registers the actual Server Component for RSC payload generation
  • Server bundle -- wraps the component for streaming SSR
  • Client bundle -- registers a placeholder that fetches the RSC payload from the server

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.

Quick-Start Migration Strategy

Tailored for React on Rails' multi-root architecture:

  1. Prepare your app -- set up the RSC infrastructure, add 'use client' to all component entry points, and switch to streaming rendering. The app works identically -- nothing changes yet.
  2. Pick a component and push the boundary down -- move 'use client' from the root component to its interactive children, letting parent components become Server Components.
  3. Adopt advanced patterns -- add Suspense boundaries, stream_react_component for streaming SSR, and server-side data fetching.
  4. Repeat for each registered component -- migrate components one at a time, in any order.

This approach lets you migrate incrementally, one component at a time, without ever breaking your app.

Component Audit Checklist

Before you start, audit your components using this classification:

CategoryCriteriaAction
Server-ready (green)No hooks, no browser APIs, no event handlersRemove 'use client' -- these are Server Components by default
Refactorable (yellow)Mix of data fetching and interactivitySplit into a Server Component (data) + Client Component (interaction)
Client-only (red)Uses useState, useEffect, event handlers, browser APIsKeep 'use client' -- these remain Client Components

Migration Readiness Checklist

Before starting any component migration, verify these items. Skipping them is the most common source of wasted debugging time:

Infrastructure

  • React 19 installed -- both react and react-dom at 19.x, with matching versions (yarn why react shows no duplicates)
  • Node renderer configured -- RSC requires NodeRenderer, not ExecJS. If config.server_renderer is not set to "NodeRenderer", migrate first
  • react-on-rails-rsc 19.0.4+ -- earlier versions vendored stale React builds. Check with yarn why react-on-rails-rsc
  • Three webpack bundles building -- client, server, and RSC bundles all compile without errors
  • RSC manifests generated -- react-client-manifest.json and react-server-client-manifest.json exist in your webpack output directory
  • RSC payload route mounted -- rsc_payload_route in config/routes.rb
  • Procfile.dev updated -- separate watcher process for the RSC bundle (HMR=true RSC_BUNDLE_ONLY=yes bin/shakapacker --watch)

Common Pre-Migration Mistakes

These mistakes account for the majority of setup failures:

MistakeSymptomFix
Missing rsc_payload_route in routes404 on RSC payload requestsAdd 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 filesCan't migrate components individuallyMove 'use client' to each component source file (Step 5)
Missing 'use client' on .server.jsx filesAuto-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_modulesCryptic hook errors, "Invalid hook call"Deduplicate with yarn why react and webpack aliases
Not switching to stream_react_componentNo streaming benefits, components render synchronouslyReplace react_component with stream_react_component in views
Missing include ReactOnRailsPro::Stream in controllerstream_view_containing_react_components undefinedAdd the concern to controllers that render React components

Prerequisites

  • Upgrading an Existing Pro App to RSC — generator-based runbook for adding RSC to an existing Pro app, including legacy webpack compatibility and verification checklist
  • React 19 Native Metadata — replace react-helmet and 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.

References