Skip to content

Commit 8140bb4

Browse files
justin808claude
andcommitted
Fix fictional API names, missing associations, form_with, and CSRF patterns in RSC docs
Replace stream_react_component_with_async_props (fictional, 18 occurrences) with the real stream_react_component helper. Replace getReactOnRailsAsyncProp (fictional, 38 occurrences) with direct prop access. Remove WithAsyncProps type references. Rewrite ERB examples to pass all data as regular props instead of using the nonexistent block/emit pattern. Fix as_json calls that access nested associations (specs, reviews) by adding include: option. Add local: true to form_with example for Rails < 6.1 compatibility. Replace CSRF token prop pattern with ReactOnRails.authenticityToken() in three form examples. Fixes #2802 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe1d0f4 commit 8140bb4

7 files changed

Lines changed: 256 additions & 442 deletions

docs/oss/migrating/migrating-to-rsc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Tailored for React on Rails' multi-root architecture:
112112

113113
1. **[Prepare your app](rsc-preparing-app.md)** -- 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.
114114
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.
115-
3. **Adopt advanced patterns** -- add Suspense boundaries, [async props](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro) for streaming data from Rails, and server-side data fetching.
115+
3. **Adopt advanced patterns** -- add Suspense boundaries, [`stream_react_component`](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro) for streaming SSR, and server-side data fetching.
116116
4. **Repeat for each registered component** -- migrate components one at a time, in any order.
117117

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

docs/oss/migrating/rsc-component-patterns.md

Lines changed: 37 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export default function ProductPage({ productId }) {
169169
```erb
170170
<%# ERB view — Rails passes the data as props %>
171171
<%= stream_react_component("ProductPage",
172-
props: { product: @product.as_json }) %>
172+
props: { product: @product.as_json(include: [:specs, :reviews]) }) %>
173173
```
174174
175175
```jsx
@@ -329,49 +329,34 @@ export default function Homepage() {
329329
330330
**Key insight:** `Homepage` (a Server Component) is the component that imports and renders `Header`, `MainContent`, and `Footer`. Since `Homepage` owns these children, they remain Server Components -- even though they're visually nested inside the Client Component `ColorProvider`.
331331

332-
## Pattern 4: Async Props with Suspense
332+
## Pattern 4: Streaming with `stream_react_component`
333333

334-
In React on Rails, use async props to stream data progressively. Each async prop streams to the browser independently as it becomes ready, and Suspense boundaries show fallbacks until the data arrives:
334+
In React on Rails, `stream_react_component` uses React's streaming SSR (`renderToPipeableStream`) to deliver HTML progressively. Rails passes all data as props, and the streaming infrastructure delivers the rendered output efficiently:
335335
336336
```erb
337-
<%# ERB view — sync props render the shell, async props stream in %>
338-
<%= stream_react_component_with_async_props("Dashboard",
339-
props: { title: "Dashboard" }) do |emit|
340-
emit.call("stats", DashboardStats.compute.as_json)
341-
emit.call("revenue", RevenueChart.data.as_json)
342-
emit.call("orders", Order.recent.as_json)
343-
end %>
337+
<%# ERB view — Rails passes all data as props %>
338+
<%= stream_react_component("Dashboard",
339+
props: { title: "Dashboard",
340+
stats: DashboardStats.compute.as_json,
341+
revenue: RevenueChart.data.as_json,
342+
orders: Order.recent.as_json }) %>
344343
```
345344
346345
```jsx
347346
// Dashboard.jsx -- Server Component
348-
import { Suspense } from 'react';
349-
import { StatsSkeleton, ChartSkeleton, TableSkeleton } from './Skeletons';
350-
351-
export default function Dashboard({ title, getReactOnRailsAsyncProp }) {
352-
const statsPromise = getReactOnRailsAsyncProp('stats');
353-
const revenuePromise = getReactOnRailsAsyncProp('revenue');
354-
const ordersPromise = getReactOnRailsAsyncProp('orders');
355-
347+
export default function Dashboard({ title, stats, revenue, orders }) {
356348
return (
357349
<div>
358350
<h1>{title}</h1>
359-
<Suspense fallback={<StatsSkeleton />}>
360-
<Stats statsPromise={statsPromise} />
361-
</Suspense>
362-
<Suspense fallback={<ChartSkeleton />}>
363-
<RevenueChart revenuePromise={revenuePromise} />
364-
</Suspense>
365-
<Suspense fallback={<TableSkeleton />}>
366-
<RecentOrders ordersPromise={ordersPromise} />
367-
</Suspense>
351+
<Stats stats={stats} />
352+
<RevenueChart revenue={revenue} />
353+
<RecentOrders orders={orders} />
368354
</div>
369355
);
370356
}
371357
372-
// Stats.jsx -- Async Server Component (awaits the streamed prop)
373-
export default async function Stats({ statsPromise }) {
374-
const stats = await statsPromise;
358+
// Stats.jsx -- Server Component (renders directly from props)
359+
export default function Stats({ stats }) {
375360
return (
376361
<div>
377362
<span>Revenue: {stats.revenue}</span>
@@ -381,63 +366,60 @@ export default async function Stats({ statsPromise }) {
381366
}
382367
```
383368
384-
Each `<Suspense>` boundary enables independent streaming -- the user sees content progressively as each async prop resolves, rather than waiting for the slowest query.
369+
`stream_react_component` streams the rendered HTML progressively to the browser. All data is available as props -- no client-side fetching or loading states needed.
385370
386-
## Pattern 5: Async Props to Client Components via `use()`
371+
## Pattern 5: Server Data to Interactive Client Components
387372
388-
Pass an async prop promise to a Client Component that resolves it with the `use()` hook. This lets data stream from Rails while the Client Component handles interactivity:
373+
Pass server-fetched data from Rails to a Client Component that adds interactivity. The Server Component receives the data as props and passes it to the Client Component:
389374
390375
```erb
391-
<%# ERB view — sync props render the shell, comments stream in %>
392-
<%= stream_react_component_with_async_props("PostPage",
393-
props: { title: post.title, body: post.body }) do |emit|
394-
emit.call("comments", post.comments.includes(:author).as_json)
395-
end %>
376+
<%# ERB view — Rails passes all data as props %>
377+
<%= stream_react_component("PostPage",
378+
props: { title: post.title,
379+
body: post.body,
380+
comments: post.comments.includes(:author).as_json }) %>
396381
```
397382
398383
```jsx
399384
// PostPage.jsx -- Server Component
400-
import { Suspense } from 'react';
401385
import Comments from './Comments';
402386
403-
export default function PostPage({ title, body, getReactOnRailsAsyncProp }) {
404-
const commentsPromise = getReactOnRailsAsyncProp('comments');
405-
387+
export default function PostPage({ title, body, comments }) {
406388
return (
407389
<article>
408390
<h1>{title}</h1>
409391
<p>{body}</p>
410-
<Suspense fallback={<p>Loading comments...</p>}>
411-
<Comments commentsPromise={commentsPromise} />
412-
</Suspense>
392+
<Comments comments={comments} />
413393
</article>
414394
);
415395
}
416396
```
417397
418-
> **Requires React 19+.** The `use(promise)` pattern for server-to-client promise handoff is not available in React 18.
419-
420398
```jsx
421-
// Comments.jsx -- Client Component
399+
// Comments.jsx -- Client Component (adds interactivity like reply buttons)
422400
'use client';
423401
424-
import { use } from 'react';
402+
import { useState } from 'react';
403+
404+
export default function Comments({ comments }) {
405+
const [expanded, setExpanded] = useState({});
425406
426-
export default function Comments({ commentsPromise }) {
427-
const comments = use(commentsPromise); // Resolves the promise
428407
return (
429408
<ul>
430409
{comments.map((c) => (
431-
<li key={c.id}>{c.text}</li>
410+
<li key={c.id}>
411+
{c.text}
412+
<button onClick={() => setExpanded((prev) => ({ ...prev, [c.id]: !prev[c.id] }))}>
413+
{expanded[c.id] ? 'Collapse' : 'Reply'}
414+
</button>
415+
</li>
432416
))}
433417
</ul>
434418
);
435419
}
436420
```
437421
438-
**Benefits:** The post title and body render immediately as sync props. Comments stream in when Rails calls `emit.call("comments", ...)`. The Client Component resolves the promise with `use()` and can add interactivity (e.g., reply buttons).
439-
440-
> **Warning:** Never create promises inside Client Components for `use()` -- this causes the "uncached promise" runtime error. See [Common `use()` Mistakes](rsc-data-fetching.md#common-use-mistakes-in-client-components) for why and what to do instead.
422+
**Benefits:** The Server Component handles data display with zero JavaScript cost. The Client Component receives pre-fetched data as props and adds only the interactivity it needs (reply buttons, expand/collapse).
441423
442424
## Decision Guide: Server or Client Component?
443425

docs/oss/migrating/rsc-context-and-state.md

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default function Providers({ children, user }) {
9595
<%# ERB view — Rails passes the data as props %>
9696
<%= stream_react_component("ProductPage",
9797
props: { user: current_user.as_json(only: [:id, :name]),
98-
product: @product.as_json }) %>
98+
product: @product.as_json(include: [:specs, :reviews]) }) %>
9999
```
100100

101101
```jsx
@@ -122,47 +122,34 @@ export default function ProductPage({ user, product }) {
122122

123123
## Pattern 3: Streaming Slow Data with Async Props
124124

125-
> **Note:** This section covers a cross-cutting concern (data fetching via async props) that affects how you structure context and state. For the full treatment of data fetching patterns, see [Data Fetching Migration](rsc-data-fetching.md).
125+
> **Note:** This section covers a cross-cutting concern (data fetching via `stream_react_component`) that affects how you structure context and state. For the full treatment of data fetching patterns, see [Data Fetching Migration](rsc-data-fetching.md).
126126
127-
In React on Rails, data comes from Rails as props. Some data is available immediately (user session, page title), but other data requires expensive queries (analytics, recommendations, external APIs). With **async props**, you send the fast data as regular props so the shell renders immediately, and stream the slow data in the background as it becomes ready.
127+
In React on Rails, data comes from Rails as props. Rails fetches all data in the controller and passes it to `stream_react_component`, which uses React's streaming SSR to deliver the rendered HTML progressively.
128128

129129
```erb
130-
<%= stream_react_component_with_async_props("ProductPage",
131-
props: { name: product.name, price: product.price }) do |emit|
132-
# These run in the background while the shell renders
133-
emit.call("reviews", product.reviews.includes(:author).as_json)
134-
emit.call("recommendations", RecommendationService.for(product).as_json)
135-
end %>
130+
<%= stream_react_component("ProductPage",
131+
props: { name: product.name, price: product.price,
132+
reviews: product.reviews.includes(:author).as_json,
133+
recommendations: RecommendationService.for(product).as_json }) %>
136134
```
137135

138-
The component renders its shell (`name`, `price`) instantly. Each async prop streams in when Rails finishes computing it, filling in Suspense boundaries progressively:
136+
The component renders with all data available as props. `stream_react_component` uses React's streaming SSR to deliver the HTML progressively:
139137

140138
```jsx
141139
// ProductPage.jsx -- Server Component
142-
import { Suspense } from 'react';
143-
144-
export default function ProductPage({ name, price, getReactOnRailsAsyncProp }) {
145-
const reviewsPromise = getReactOnRailsAsyncProp('reviews');
146-
const recommendationsPromise = getReactOnRailsAsyncProp('recommendations');
147-
140+
export default function ProductPage({ name, price, reviews, recommendations }) {
148141
return (
149142
<div>
150143
<h1>{name}</h1>
151144
<p>${price}</p>
152145

153-
<Suspense fallback={<p>Loading reviews...</p>}>
154-
<Reviews reviewsPromise={reviewsPromise} />
155-
</Suspense>
156-
<Suspense fallback={<p>Loading recommendations...</p>}>
157-
<Recommendations itemsPromise={recommendationsPromise} />
158-
</Suspense>
146+
<ReviewList reviews={reviews} />
147+
<RecommendationList items={recommendations} />
159148
</div>
160149
);
161150
}
162151

163-
// Server Component -- awaits the streamed prop
164-
async function Reviews({ reviewsPromise }) {
165-
const reviews = await reviewsPromise;
152+
function ReviewList({ reviews }) {
166153
return (
167154
<ul>
168155
{reviews.map((r) => (
@@ -173,11 +160,11 @@ async function Reviews({ reviewsPromise }) {
173160
}
174161
```
175162

176-
`getReactOnRailsAsyncProp(key)` returns a cached Promise (same object on repeated calls), so you can pass it to multiple children -- Server Components `await` it, Client Components resolve it with `use()`. No `React.cache()` or Context wiring needed.
163+
All props are available immediately in the component. `stream_react_component` handles progressive HTML delivery via React's `renderToPipeableStream`.
177164

178165
> **Note:** `React.cache()` is only available in React Server Component environments. It is not available in client components or non-RSC server rendering (e.g., `renderToString`).
179166
180-
> For the full async props API, TypeScript typing, and more examples, see [Data Fetching in React on Rails Pro](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro).
167+
> For more streaming patterns and examples, see [Data Fetching in React on Rails Pro](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro).
181168
182169
## Migrating Global State Libraries
183170

@@ -281,13 +268,13 @@ Zustand and Jotai follow the same pattern as Redux: keep all store access in Cli
281268

282269
RSC reduces the need for global state libraries because data fetching moves to the server:
283270

284-
| Use Case | Recommended Approach |
285-
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
286-
| Server data (read-only display) | Rails controller props → Server Component renders directly |
287-
| Server data (slow, shouldn't block the shell) | [Async props](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro) with Suspense boundaries |
288-
| Server data (with client cache/revalidation) | TanStack Query with prefetch + hydrate |
289-
| Client UI state (modals, forms, selections) | `useState` / Context in Client Components |
290-
| Complex client state (undo/redo, shared across many components) | Redux Toolkit in Client Components |
271+
| Use Case | Recommended Approach |
272+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
273+
| Server data (read-only display) | Rails controller props → Server Component renders directly |
274+
| Server data (slow, shouldn't block the shell) | [Streaming](rsc-data-fetching.md#data-fetching-in-react-on-rails-pro) with `stream_react_component` |
275+
| Server data (with client cache/revalidation) | TanStack Query with prefetch + hydrate |
276+
| Client UI state (modals, forms, selections) | `useState` / Context in Client Components |
277+
| Complex client state (undo/redo, shared across many components) | Redux Toolkit in Client Components |
291278

292279
## Specific Provider Patterns
293280

@@ -443,8 +430,8 @@ export default function InteractiveFilters() {
443430

444431
### Phase 3: Replace Server-Side Context Usage
445432

446-
7. Replace `useContext` in data-fetching components with Rails controller props or async props
447-
8. For data shared between Server and Client Components, pass async prop promises directly as props (no Context needed)
433+
7. Replace `useContext` in data-fetching components with Rails controller props
434+
8. For data shared between Server and Client Components, pass data directly as props (no Context needed)
448435
9. Remove Context providers that only existed to pass server data down the tree
449436

450437
### Phase 4: State Management Libraries

0 commit comments

Comments
 (0)