docs/pro/react-server-components/upgrading-existing-pro-app.md
This guide walks you through adding React Server Components to an existing React on Rails Pro application using the standalone react_on_rails:rsc generator. If you're starting a new app from scratch, use rails g react_on_rails:install --rsc instead.
For React-side migration patterns (restructuring components, Context, data fetching, etc.), see the RSC Migration Guide series. This page covers only the infrastructure upgrade.
Before running the generator, verify your environment:
| Requirement | Check command | Expected |
|---|---|---|
| React on Rails Pro gem | bundle show react_on_rails_pro | v16.4.0+ |
| React on Rails gem | bundle show react_on_rails | v16.4.0+ |
| React on Rails Pro npm | npm ls react-on-rails-pro / yarn why react-on-rails-pro / pnpm list react-on-rails-pro / bun pm why react-on-rails-pro | Matches gem version |
| React version | npm ls react / yarn why react / pnpm list react / bun pm why react | 19.2.x with patch >= 19.2.7 |
| React DOM version | npm ls react-dom / yarn why react-dom / pnpm list react-dom / bun pm why react-dom | Must match react version |
react-on-rails-rsc | npm ls react-on-rails-rsc / yarn why react-on-rails-rsc / pnpm list react-on-rails-rsc / bun pm why react-on-rails-rsc | 19.2.x with patch >= 19.2.1 |
| Node.js | node --version | 18+ |
| Pro initializer exists | ls config/initializers/react_on_rails_pro.rb | File exists |
| Node renderer configured | Check react_on_rails_pro.rb for server_renderer = "NodeRenderer" | NodeRenderer enabled |
If React is outside the supported 19.2.x range or below 19.2.7, upgrade it first:
pnpm add react@~19.2.7 react-dom@~19.2.7 [email protected]
# or: yarn add react@~19.2.7 react-dom@~19.2.7 [email protected]
# or: npm install react@~19.2.7 react-dom@~19.2.7 [email protected]
React 19.2.x with patch >= 19.2.7 is required for the React on Rails Pro 17 RSC path. React 19.0.x is no longer a supported Pro RSC runtime line in v17.
[!NOTE] The RSC generator uses the coordinated React 19.2.7 / stable
[email protected]package set. Later stable 19.2.x packages with patch >= 19.2.1 remain on the supported package line.
[!NOTE] Keep React, React DOM, and
react-on-rails-rscupgraded as a coordinated set. The RSC bundler APIs are version-coupled, so do not bumpreact-on-rails-rscby itself.
The generator-managed RSC version is what goes in your app's package.json. Separately, the Pro package itself declares an optional peer range, which is broader on purpose:
[!NOTE] The Pro package's optional
react-on-rails-rscpeer range is>= 19.2.1 < 20.0.0; prereleases do not satisfy this stable floor. The Pro node renderer also checks the installedreact-on-rails-rsc, React, and React DOM versions at startup and hard-errors on unsupported combinations. SetREACT_ON_RAILS_PRO_DISABLE_VERSION_CHECK=1only as an emergency rollout escape hatch; it downgrades that startup error to a warning.
Before running the generator, audit your existing components to identify which ones use client-side APIs. When RSC is enabled, any component without 'use client' is automatically classified as a React Server Component. Components that use client APIs will break if misclassified.
Components that use any of the following must have 'use client':
import { ... } from 'react'): useState, useEffect, useLayoutEffect, useInsertionEffect, useContext, useRef, useImperativeHandle, useReducer, useCallback, useMemo, useTransition, useDeferredValue, useId, useSyncExternalStore, useOptimisticimport { ... } from 'react-dom'): useFormStatusReactOnRails.getStore(), ReactOnRails.authenticityToken()useSelector, useDispatch, connect(), <Provider>useNavigate, useLocation, useParamsStaticRouter: these are SSR wrappers, not RSC server components — see the .server.jsx naming collision belowonClick, onChange, onSubmit, etc.window, document, localStorage[!NOTE]
fetch,Headers,Request,Response,AbortController, andAbortSignaldo not require'use client', but they are not available inside the node renderer VM by default. If your existing Server Components callfetch()directly, bundle an HTTP client (node-fetchv2 orundici) or inject fetch globals viaadditionalContextat renderer startup. See Node Renderer Runtime Globals.
.server.jsx naming collisionIf your app uses React on Rails' auto-bundling with .client.jsx / .server.jsx file pairs, be aware of a naming collision:
.server.jsx means "include this file in the server bundle for SSR." These files typically contain traditional SSR logic using StaticRouter, ReactOnRails.getStore(), etc.These are completely different concepts. A .server.jsx file is not a React Server Component -- it's a file included in the server bundle. Without 'use client', the RSC infrastructure will misclassify it as a Server Component, causing runtime errors.
Important: Do not rename
.server.jsxfiles to.ssr.jsx— React on Rails' auto-bundling relies on the.server.suffix to detect server-bundle entries (Dir.glob("*.server.*")inpacks_generator.rb). Renaming would silently drop the file from server bundle registration. Instead, add'use client'to these files so the RSC infrastructure classifies them correctly while preserving auto-bundling behavior.
When RSC is enabled, React on Rails classifies components at build time:
'use client' → registered via ReactOnRails.register() → Client Component'use client' → registered via registerServerComponent() → Server ComponentThere is no warning when a component is auto-classified as a server component. If it uses client APIs, it will fail at runtime with errors like "useState is not a function" or "Cannot read properties of undefined."
Before proceeding to Step 1:
useState, useEffect, useLayoutEffect, useInsertionEffect, useContext, useRef, useImperativeHandle, useReducer, useCallback, useMemo, useTransition, useDeferredValue, useId, useSyncExternalStore, useOptimistic, useFormStatus, useSelector, useDispatch, connect(, useNavigate, useLocation, useParams, ReactOnRails.getStore, ReactOnRails.authenticityToken.server.jsx files -- these almost certainly need 'use client'StaticRouter (SSR wrapper, not a client API — but the file likely uses other client APIs)window, document) without 'use client'When in doubt, add
'use client'. Starting with all components as Client Components is safe and preserves existing behavior. You can remove the directive later when you're ready to convert a component to a Server Component.
rails generate react_on_rails:rsc
# or with TypeScript:
rails generate react_on_rails:rsc --typescript
The generator is safe to re-run -- new files are skipped and existing-file patches are applied only when the target pattern is not already present. If a transform cannot be applied (e.g. because your config has been customized), the generator reports a warning but continues.
| File | Purpose |
|---|---|
config/webpack/rscWebpackConfig.js | RSC webpack bundle configuration |
app/javascript/src/HelloServer/ror_components/HelloServer.jsx (or .tsx) | React on Rails registration entry-point |
app/javascript/src/HelloServer/components/HelloServer.jsx (or .tsx) | Example Server Component |
app/javascript/src/HelloServer/components/LikeButton.jsx (or .tsx) | Example Client Component used by HelloServer |
app/controllers/hello_server_controller.rb | Controller for the example RSC page |
app/views/hello_server/index.html.erb | View for the example RSC page |
| File | Change |
|---|---|
config/webpack/serverWebpackConfig.js | Adds RSCWebpackPlugin, rscBundle parameter to configureServer |
config/webpack/clientWebpackConfig.js | Adds RSCWebpackPlugin |
config/webpack/ServerClientOrBoth.js | Adds rscWebpackConfig import, RSC_BUNDLE_ONLY guard |
config/initializers/react_on_rails_pro.rb | Adds RSC configuration block |
config/routes.rb | Adds rsc_payload_route and hello_server route |
Procfile.dev | Adds RSC bundle watcher process |
package.json | Adds react-on-rails-rsc dependency |
The generator automatically handles both webpack export shapes used across Pro app versions. No manual action is needed, but understanding the difference helps with troubleshooting.
Recent versions of the React on Rails Pro generator export an object from serverWebpackConfig.js (introduced via PR 2424):
// config/webpack/serverWebpackConfig.js
module.exports = {
default: configureServer,
extractLoader,
};
And ServerClientOrBoth.js destructures the import:
const { default: serverWebpackConfig } = require('./serverWebpackConfig');
Older Pro apps or apps upgraded from OSS export a plain function. These apps must be on
react_on_rails_pro v16.4.0+ before adding RSC (see Prerequisites); once upgraded, no
manual export-shape rewrite is required:
// config/webpack/serverWebpackConfig.js
module.exports = configureServer;
And ServerClientOrBoth.js imports directly:
const serverWebpackConfig = require('./serverWebpackConfig');
The generated rscWebpackConfig.js includes backward-compatible imports that work with either shape:
const serverWebpackModule = require('./serverWebpackConfig');
// Works with both export shapes
const serverWebpackConfig = serverWebpackModule.default || serverWebpackModule;
const extractLoader =
serverWebpackModule.extractLoader ||
((rule, loaderName) => {
// Fallback implementation when extractLoader is not exported
if (!Array.isArray(rule.use)) return null;
return rule.use.find((item) => {
const testValue = typeof item === 'string' ? item : item.loader;
return testValue && testValue.includes(loaderName);
});
});
If extractLoader is not exported (legacy shape), the RSC config provides a built-in fallback that scans webpack rule arrays the same way. This means legacy apps do not need to modify their serverWebpackConfig.js export shape.
After running the generator, verify the setup works end-to-end.
# Build all three bundles
bin/shakapacker
# Or build individually to isolate errors
CLIENT_BUNDLE_ONLY=true bin/shakapacker
SERVER_BUNDLE_ONLY=true bin/shakapacker
RSC_BUNDLE_ONLY=true bin/shakapacker
All three builds should succeed without errors.
Verify these files exist in the expected locations:
react-client-manifest.json -- in your webpack output directory (typically public/webpack/development/ or public/webpack/production/)react-server-client-manifest.json -- in the same webpack output directoryrsc-bundle.js -- in your server_bundle_output_path directory (default: ssr-generated/)rails routes | grep rsc_payload
Should show the RSC payload endpoint (e.g., GET /rsc_payload/:component_name).
Start the dev server and visit the example page:
bin/dev
# Visit http://localhost:3000/hello_server
The page should render the HelloServer component with:
Verify all processes start correctly in Procfile.dev:
bin/dev
You should see log output from:
The RSC generator requires the Pro gem. If you see this error, ensure react_on_rails_pro is in your Gemfile:
gem 'react_on_rails_pro'
Then run bundle install before retrying the generator.
If the RSC bundle build fails but server and client builds succeed, the issue is likely in rscWebpackConfig.js. Common causes:
react-on-rails-rsc package: Run npm install [email protected] / yarn add [email protected] / pnpm add [email protected], or install a later stable 19.2.x package with patch >= 19.2.1.react-on-rails-rsc version mismatch: RSC currently requires React 19.2.x with patch >= 19.2.7 and react-on-rails-rsc 19.2.x with patch >= 19.2.1. Check with npm ls react react-dom react-on-rails-rsc, yarn why react / yarn why react-dom / yarn why react-on-rails-rsc, or pnpm list react react-dom react-on-rails-rscserverWebpackConfig.js was heavily customized, the generator's transforms may not apply cleanly. See Preparing Your App: Step 4 for the underlying intent of each webpack changeIf react-client-manifest.json or react-server-client-manifest.json are missing after building:
RSCWebpackPlugin was added to both clientWebpackConfig.js and serverWebpackConfig.jsclientReferences in the plugin config points to a directory that contains your component source files'use client' directive -- the plugin only generates entries for files it detects as Client ComponentsIf SSR hangs with large RSC payloads, you may need to update react-on-rails-pro. See Stream Backpressure Deadlock for details.
After the infrastructure is in place, migrate your React components:
'use client' to all entry points -- marks all existing components as Client Components so nothing changes yet'use client' boundaries down to leaf components