internal/planning/react-19-partial-prerendering-plan.md
Plan the work requested in Issue 2182 (the originating issue, now closed): verify React 19.2.x support and decide how React on Rails should expose any partial pre-rendering workflow that becomes practical for Rails apps. Active follow-up is tracked in Issue 3255.
This is a planning document for Issue 2182. The one package
change it has driven — the react-on-rails-rsc peer-ceiling widening — is captured in the Decision Record below; beyond
that it does not change build configuration or Pro package code.
Status: Draft | Created: 2026-04-30 | Last updated: 2026-06-03 | Originated from: Issue 2182 (closed) | Active tracking: Issue 3255
Resolved 2026-06-03 by @justin808. This closes the two package-range questions split out of Issue 3255 and tracked in Issue 3486.
Minimum supported React version — keep React 18 (and 16/17) support. The OSS react-on-rails and Pro
react-on-rails-pro react / react-dom peer ranges stay at >= 16. React on Rails v17 is a Ruby-baseline release
(Ruby 3.3+ required), not a React-baseline release, and v17 actively ships features for React 16/17 consumers (the
react-on-rails/webpackHelpers reactDomClientWarning export). The React 19 baseline that RSC requires is expressed
where it belongs — at the RSC boundary, through the optional react-on-rails-rsc peer dependency — so non-RSC
SSR/streaming users are not forced off React 18. The packages/react-on-rails/src/ReactDOMServer.cts React 16/17 shim
therefore stays; its removal remains gated on a future, explicitly-signed-off baseline bump.
react-on-rails-rsc peer ceiling — widen to >= 19.0.2 < 20.0.0. The previous <= 19.2.3 upper bound was a
precautionary verified-patch ceiling, not a recorded API incompatibility. No specific API risk was found, so per the
issue default the ceiling widens to the full React 19 line. This stops the peer from rejecting a future 19.2.4 patch
or 19.3.x minor release of react-on-rails-rsc.
The react / react-dom peers (>= 16) and the react-on-rails-rsc peer (>= 19.0.2 < 20.0.0) are intentionally
not identical: the RSC peer is optional and only constrains the React-19-only RSC path. This is the resolved, recorded
outcome for the Open Questions at the end of this document.
The workspace package ranges already allow React 19.2.x through ^19.0.3 for react and react-dom, plus ^19.0.4
for react-on-rails-rsc (the React on Rails RSC integration package, not a React-team package), in the root, dummy app,
and Pro dummy app package manifests. To see what versions are currently resolved, run
pnpm list -r --depth=0 react react-dom and
pnpm --filter react-on-rails-pro list --depth=0 react-on-rails-rsc from the repo root. Note:
react_on_rails_pro/spec/execjs-compatible-dummy is intentionally pinned to React 18 through pnpm overrides for app>react
and app>react-dom; verification should
confirm whether that workspace stays on React 18 during this work. That means the first implementation step is
verification, not necessarily a broad package-range change.
Note: packages/react-on-rails-pro/package.json now sets the react-on-rails-rsc peer dependency ceiling to
>= 19.0.2 < 20.0.0 (resolved by the Issue 3486 decision
recorded above; widened from the earlier precautionary <= 19.2.3 ceiling). These 19.x version numbers apply to the
react-on-rails-rsc package, not to React itself: react-on-rails-rsc's major and minor numbers track the React
release line it targets, so this range constrains the RSC integration package, not the React peer dependency. The earlier
<= 19.2.3 upper bound was a precautionary verified-patch ceiling from the planning pass, not a recorded React API
incompatibility; verification found no specific API risk, so the ceiling was widened to the full React 19 line per the
issue default. A future 19.2.4 patch or 19.3.x minor release of react-on-rails-rsc is now accepted. Pre-release
React versions should remain outside the recommended range unless the verification record explicitly tests them. The Pro
package react and react-dom peer dependency ranges stay at >= 16: React 18 support is retained, and the React 19
baseline that RSC requires is expressed through the optional react-on-rails-rsc peer rather than by tightening the
react / react-dom peers. Owner: @justin808 | Status: resolved 2026-06-03 (see Decision Record above).
Use a dedicated branch for the actual version verification work:
Review the React 19.2.x changelog and React 19 upgrade guide for breaking changes, deprecations, and new APIs that could affect React on Rails SSR, streaming, RSC, or hydration integration.
Audit renderToString and renderToStaticMarkup call sites in React on Rails SSR paths; React 18+ renders Suspense
fallbacks synchronously in renderToString instead of suspending, which can silently change output without an error.
For each call site in packages/react-on-rails/src/serverRenderReactComponent.ts,
packages/react-on-rails/src/handleError.ts, and any generated bundles found by
grep -rE "renderToString|renderToStaticMarkup" packages/ --include="*.js" --include="*.mjs" --include="*.cjs" --include="*.ts" --include="*.tsx" --include="*.cts" --include="*.mts"
run from the repo root,
document whether a Suspense-containing tree could plausibly be passed by a user render function today, then either
open a follow-up migration ticket or record why the current usage is acceptable.
If the Issue 3255 minimum-React-version decision drops React 16/17 support, remove
packages/react-on-rails/src/ReactDOMServer.cts. Otherwise, confirm the file's existing removal comment remains the
accepted exit criterion and close this task.
Run pnpm install from a freshly cloned or freshly cleaned checkout with no existing node_modules, then confirm
React, React DOM, and react-on-rails-rsc resolve to compatible versions. Capture the exact
pnpm list -r --depth=0 react react-dom and
pnpm --filter react-on-rails-pro list --depth=0 react-on-rails-rsc output in the verification record, such as a
comment on Issue 3255, so the tested React 19.2.x patch version is auditable. Also verify that the resolved
react-on-rails-rsc version satisfies both the root ^19.0.4 range and the
packages/react-on-rails-pro/package.json peer dependency ceiling >= 19.0.2 < 20.0.0; a mismatch means the ceiling
must be widened before workspace installs stay consistent across OSS and Pro.
Run package checks. Type checking catches breaking API changes — @types/react shifts such as removal of the
implicit children prop from React.FC and a stricter useRef return type, plus react-dom/server changes such
as renderToPipeableStream and renderToReadableStream through @types/react-dom — lint enforces package style,
and tests exercise the runtime paths:
pnpm run lint from the repo root (ESLint only; per-package type-check and tests follow)pnpm --filter react-on-rails run type-checkpnpm --filter react-on-rails run testpnpm --filter create-react-on-rails-app run type-checkpnpm --filter create-react-on-rails-app run testpnpm --filter react-on-rails-pro run type-check (requires Pro runtime prerequisites such as license or env setup)pnpm --filter react-on-rails-pro run test (requires Pro runtime prerequisites such as license or env setup)pnpm --filter react-on-rails-pro-node-renderer run type-check (requires Pro runtime prerequisites such as license
or env setup)pnpm --filter react-on-rails-pro-node-renderer run test (requires Pro runtime prerequisites such as license or env
setup)react_on_rails_pro/spec/execjs-compatible-dummy needs a dedicated React 18 test run as part of the
acceptance criteria.@tanstack/react-router version used by the Pro package still satisfies its React 19 compatibility
matrix, and make the Pro ./tanstack-router export part of the RSC boundary verification. If local Pro prerequisites
are unavailable, record the PR's Pro CI result as the proxy and open a follow-up only if that CI does not cover the
export.Run Ruby checks that exercise SSR and generated apps:
bundle exec rubocopbundle exec rake rbs:validatecd react_on_rails && bundle exec rspec spec/react_on_rails/ for gem-side rendering, doctor, and generator coveragecd react_on_rails/spec/dummy && bundle exec rspec spec/requests spec/system spec/packs_generator_spec.rb for dummy
SSR and generator integration pathsreact_on_rails_pro/ checked out):
cd react_on_rails_pro/spec/dummy
bundle exec rspec spec/requests/rsc_payload_spec.rb spec/requests/server_render_check_spec.rb spec/system/renderer_integration_spec.rb
.claude/docs/replicating-ci-failures.md (Claude Code agent reference) when mapping a failed CI job back to a
narrower local command; human contributors can also use the failing CI job name from the GitHub Actions log to scope
the local repro.Ensure Playwright browsers are installed, then run E2E coverage:
cd react_on_rails/spec/dummy && pnpm playwright install --with-depscd react_on_rails/spec/dummy && pnpm test:e2e for OSS dummy SSR and hydration pathscd react_on_rails_pro/spec/dummy && pnpm playwright install --with-depscd react_on_rails_pro/spec/dummy && pnpm run e2e-test for Pro stream_react_component and RSC payload paths.claude/docs/playwright-e2e-testing.md (Claude Code agent reference) for additional notes; the commands above
are the canonical OSS dummy setup for human contributors.Run the generated-app suite. Prefer a fresh clone. If a fresh clone is not practical, use a disposable worktree so the current checkout and gitignored files are untouched:
git worktree list # confirm no stale /tmp/ror-verify entry from a prior interrupted run
git worktree add /tmp/ror-verify HEAD # detached HEAD is intentional for read-only verification
(cd /tmp/ror-verify && pnpm install)
cd /tmp/ror-verify/react_on_rails && bundle exec rake run_rspec:shakapacker_examples # full suite across all example apps (latest + all pinned React versions)
After the suite runs, remove the worktree from the original checkout:
git worktree remove /tmp/ror-verify
If neither a fresh clone nor a disposable worktree is practical, use the repo root only after stashing or committing tracked
and non-ignored in-progress work. Note: git stash -u saves untracked non-ignored files, but does not save gitignored
files.
Warning:
git clean -fdxdeletes all untracked files, including gitignored files, and cannot be undone. Move or back up gitignored files, such as.env, local credentials, or generated certs, before using the fallback below.
git stash -u
git clean -ndx # dry run — review the printed list before running the destructive command below
git clean -fdx
pnpm install
cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples # full suite across all example apps (latest + all pinned React versions)
Note: bundle exec rake shakapacker_examples:gen_all only generates apps; a separate run_rspec:* task must run their
tests.
Confirm docs that mention explicit React versions are either updated or intentionally left on older minimum-version examples.
Confirm the secondary and backup reviewers named in the Resolved Decisions section are still available before opening the first implementation PR.
If any verification step fails, capture the exact command and failure in a comment on Issue 3255, then apply this default decision rule (Owner: @justin808 for all blocking calls):
renderToString semantic shift: open a migration ticket,
document the change, and do not block the range work.Before implementation, define the feature in React on Rails terms instead of adopting another framework's terminology too loosely:
Note: React's official experimental PPR API in canary releases is not required for the patterns described here. This plan targets approaches achievable with stable React 19.x unless a specific implementation step states otherwise.
Evaluate these in order:
react_component plus Suspense, and RSC
boundaries to get a static-shell or streaming-SSR-style result without new OSS public APIs. The Pro version of the
pattern can use stream_react_component for streaming delivery.run_rspec:shakapacker_examples.bundle exec rake run_rspec:shakapacker_examples) passes with React 19.2.x.internal/planning/library-benchmarking.md for tooling guidance).The five questions below were resolved in
Issue 3255 on 2026-06-02. The overarching frame: PPR splits
into two tracks, and the first example ships Track A — streaming SSR with Suspense and a fragment-cached shell, on
existing stable React 19 plus existing Pro helpers (stream_react_component, cached_stream_react_component). Track B
— RSC with "use cache", per ppr-implementation-plan.md — is the longer-term path,
tracked in Issue 3571.
Secondary reviewer (SSR-vs-RSC): Abanoub Ghadban (@AbanoubGhadban) (fallback: @justin808)
Backup reviewer (benchmarks): Abanoub Ghadban (@AbanoubGhadban) (fallback: @justin808)
stream_react_component), not RSC and not both-in-one. RSC PPR becomes Example 2 under Track B
(Issue 3571). Rationale: it ships on stable React 19 and
existing Pro helpers as docs plus one dummy-app route, rather than blocking on the multi-month Track B toolchain.cached_stream_react_component (cache_key + cache_options[:expires_in]); dynamic holes stream per
request and are never cached. Tier 2 (HTTP caching headers such as
Cache-Control: public, s-maxage, stale-while-revalidate) applies only to fully static routes with no live
holes. Tier 3 (CDN edge cache of shell + postponed state with an origin resume protocol) is split into
Issue 3572. Non-goal for v1: HTTP/CDN-caching a
streamed response that still has live per-request holes.stream_react_component; the Rails fragment cache stores it via
cached_stream_react_component, so the shell is rendered once and replayed without re-invoking the renderer. Track B
uses the same composition with richer paired artifacts (HTML prelude + opaque postponed state).library-benchmarking.md — a max-rate run for capacity plus a fixed-rate run for
meaningful TTFB/LCP latency comparison, with Bencher tracking regressions. Hold the delivery path
(ActionController::Live vs. Node Renderer streaming) constant within an A/B pair and report which was used.These remain open after Issue 3255; they were outside that issue's five-question scope. Track each one to an explicit decision before the implementation step it gates.
stream_react_component can flush into a Turbo Stream frame.
Owner: @justin808 | Target: before any implementation PR is opened>= 16). See the Decision Record at the top of this document.react-on-rails-pro peer dependency ceiling for react-on-rails-rsc stay at <= 19.2.3 or widen with the
React 19.2.x verification work?>= 19.0.2 < 20.0.0 (no specific API risk found). See the Decision Record at the top of this document.