Skip to content

Commit 9f752e3

Browse files
justin808claude
andcommitted
Replace Next.js data fetching patterns with React on Rails async props in RSC docs (#2791)
## Summary Fixes #2523 - Replace `await getProduct()` / `await getUser()` / `await getStats()` etc. with Rails controller props and `getReactOnRailsAsyncProp` across all RSC migration docs - Rewrite waterfall avoidance section to show Ruby-side `emit.call` parallelization (threads) instead of JS-side `Promise.all` / preload patterns - Replace React Query prefetch+hydrate pattern with `initialData` from Rails props - Replace SWR server fetch with Rails props as `fallbackData` - Remove all Server Actions (`'use server'`) examples and add explicit unsupported notes throughout - Reduce `React.cache()` section — note that `getReactOnRailsAsyncProp` already returns a cached promise - Rewrite progressive streaming section to use async props with ERB examples - Fix migration checklist to reference Rails props and `getReactOnRailsAsyncProp` - Add corresponding ERB view examples alongside component code examples **Affected files:** - `rsc-data-fetching.md` — major rewrite (waterfall, React Query, SWR, use() hook, React.cache(), Server Actions, hybrid pattern, streaming, checklist) - `rsc-component-patterns.md` — Patterns 1, 4, 5 + Mistake 4 note - `rsc-context-and-state.md` — Providers and Redux examples - `rsc-third-party-libs.md` — Form libraries table + Server Action form pattern - `rsc-troubleshooting.md` — serialization table, Server Action fix, testing, validation, error catalog ## Test plan - [ ] Review each code example for correctness — ERB syntax, async props API, TypeScript types - [ ] Verify all internal links still resolve (anchors may have changed) - [ ] Confirm no remaining `await getProduct()` / `await getUser()` patterns outside of the React.cache() and troubleshooting diagnostic sections - [ ] Confirm all `'use server'` references are now in warning/unsupported contexts only 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk documentation-only update, but it changes recommended migration patterns (async props, mutations, and caching) so inaccuracies could mislead adopters. > > **Overview** > Updates the RSC migration docs to **replace Next.js-style server fetching** (`async` components calling `getX()`/`Promise.all`) with **React on Rails patterns**: Rails-provided sync props plus streamed async props via `stream_react_component_with_async_props` and `getReactOnRailsAsyncProp` (with new ERB examples throughout). > > Reworks guidance for common integrations: React Query now seeds the client cache via Rails props (`initialData`), SWR uses Rails props (`fallbackData`), and the waterfall section is rewritten around Ruby-side `emit.call` sequencing/parallelization. Removes Server Actions examples and adds explicit notes that `'use server'` is unsupported in React on Rails, with updated mutation, CSRF, testing, and validation examples centered on Rails controller endpoints. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9279c97. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Migration guides now favor props-first: Rails supplies synchronous props and can stream async props into React. * New/renamed patterns: Async Props with Suspense, async-prop streaming, and Async Props → client via use(). * Updated data-fetching, React Query/SWR guidance, and progressive streaming strategies/examples. * Replaced Server Actions guidance with Rails-controller form/mutation patterns; added CSRF, testing, and validation notes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bd0de1f commit 9f752e3

5 files changed

Lines changed: 486 additions & 449 deletions

File tree

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

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,17 @@ export default function ProductPage({ productId }) {
164164
}
165165
```
166166
167-
### After: State pushed to a leaf, data fetched on server
167+
### After: State pushed to a leaf, data from Rails props
168+
169+
```erb
170+
<%# ERB view — Rails passes the data as props %>
171+
<%= stream_react_component("ProductPage",
172+
props: { product: @product.as_json }) %>
173+
```
168174
169175
```jsx
170176
// ProductPage.jsx -- Server Component (no directive)
171-
// Generic RSC example: in React on Rails, this data would typically come from
172-
// Rails props or async props. See Part 4 for the recommended fetching patterns.
173-
export default async function ProductPage({ productId }) {
174-
const product = await getProduct(productId);
175-
177+
export default function ProductPage({ product }) {
176178
return (
177179
<div>
178180
<h1>{product.name}</h1>
@@ -190,7 +192,7 @@ export default async function ProductPage({ productId }) {
190192
'use client';
191193
192194
import { useState } from 'react';
193-
import { addToCart } from '../actions'; // Server Action or API call for mutation
195+
import { addToCart } from '../api'; // Calls a Rails controller endpoint
194196
195197
export default function AddToCartButton({ productId }) {
196198
const [quantity, setQuantity] = useState(1);
@@ -327,38 +329,49 @@ export default function Homepage() {
327329
328330
**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`.
329331

330-
## Pattern 4: Async Server Components with Suspense
332+
## Pattern 4: Async Props with Suspense
331333

332-
Server Components can be `async` functions that fetch data directly. Wrap them in `<Suspense>` to stream content progressively:
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:
335+
336+
```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 %>
344+
```
333345

334346
```jsx
335347
// Dashboard.jsx -- Server Component
336348
import { Suspense } from 'react';
337-
import Stats from './Stats';
338-
import RevenueChart from './RevenueChart';
339-
import RecentOrders from './RecentOrders';
340349
import { StatsSkeleton, ChartSkeleton, TableSkeleton } from './Skeletons';
341350
342-
export default function Dashboard() {
351+
export default function Dashboard({ title, getReactOnRailsAsyncProp }) {
352+
const statsPromise = getReactOnRailsAsyncProp('stats');
353+
const revenuePromise = getReactOnRailsAsyncProp('revenue');
354+
const ordersPromise = getReactOnRailsAsyncProp('orders');
355+
343356
return (
344357
<div>
345-
<h1>Dashboard</h1>
358+
<h1>{title}</h1>
346359
<Suspense fallback={<StatsSkeleton />}>
347-
<Stats /> {/* Fetches and renders independently */}
360+
<Stats statsPromise={statsPromise} />
348361
</Suspense>
349362
<Suspense fallback={<ChartSkeleton />}>
350-
<RevenueChart /> {/* Fetches and renders independently */}
363+
<RevenueChart revenuePromise={revenuePromise} />
351364
</Suspense>
352365
<Suspense fallback={<TableSkeleton />}>
353-
<RecentOrders /> {/* Fetches and renders independently */}
366+
<RecentOrders ordersPromise={ordersPromise} />
354367
</Suspense>
355368
</div>
356369
);
357370
}
358371
359-
// Stats.jsx -- Async Server Component
360-
export default async function Stats() {
361-
const stats = await getStats(); // Direct server-side fetch
372+
// Stats.jsx -- Async Server Component (awaits the streamed prop)
373+
export default async function Stats({ statsPromise }) {
374+
const stats = await statsPromise;
362375
return (
363376
<div>
364377
<span>Revenue: {stats.revenue}</span>
@@ -368,25 +381,32 @@ export default async function Stats() {
368381
}
369382
```
370383

371-
Each `<Suspense>` boundary enables independent streaming -- the user sees content progressively as each data fetch completes, rather than waiting for the slowest query.
384+
Each `<Suspense>` boundary enables independent streaming -- the user sees content progressively as each async prop resolves, rather than waiting for the slowest query.
385+
386+
## Pattern 5: Async Props to Client Components via `use()`
372387

373-
## Pattern 5: Server-to-Client Promise Handoff
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:
374389

375-
Start a data fetch on the server but let the client resolve it. This avoids blocking the server render while still starting the fetch early:
390+
```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 %>
396+
```
376397

377398
```jsx
378-
// Page.jsx -- Server Component
399+
// PostPage.jsx -- Server Component
379400
import { Suspense } from 'react';
380401
import Comments from './Comments';
381402
382-
export default async function Page({ id }) {
383-
const post = await getPost(id); // Await critical data
384-
const commentsPromise = getComments(id); // Start but DON'T await
403+
export default function PostPage({ title, body, getReactOnRailsAsyncProp }) {
404+
const commentsPromise = getReactOnRailsAsyncProp('comments');
385405
386406
return (
387407
<article>
388-
<h1>{post.title}</h1>
389-
<p>{post.body}</p>
408+
<h1>{title}</h1>
409+
<p>{body}</p>
390410
<Suspense fallback={<p>Loading comments...</p>}>
391411
<Comments commentsPromise={commentsPromise} />
392412
</Suspense>
@@ -415,7 +435,7 @@ export default function Comments({ commentsPromise }) {
415435
}
416436
```
417437

418-
**Benefits:** The post renders immediately. Comments stream in when ready. The promise starts on the server (close to the data source) but resolves on the client.
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).
419439

420440
> **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.
421441

@@ -507,6 +527,8 @@ If your RSC page downloads unexpectedly large chunks, a shared `'use client'` co
507527
- `'use server'` marks **Server Actions** (functions callable from the client) -- NOT Server Components
508528
- Server Components are the **default** and need no directive
509529

530+
> **React on Rails note:** Server Actions (`'use server'`) are **not supported** in React on Rails. Server Actions run on the Node renderer, which has no access to Rails models, sessions, cookies, or CSRF protection. Use Rails controllers for all mutations. See [Mutations: Rails Controllers, Not Server Actions](rsc-data-fetching.md#mutations-rails-controllers-not-server-actions).
531+
510532
## Next Steps
511533

512534
- [Context, Providers, and State Management](rsc-context-and-state.md) -- how to handle Context and global state

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,21 @@ export default function Providers({ children, user }) {
9191
}
9292
```
9393

94+
```erb
95+
<%# ERB view — Rails passes the data as props %>
96+
<%= stream_react_component("ProductPage",
97+
props: { user: current_user.as_json(only: [:id, :name]),
98+
product: @product.as_json }) %>
99+
```
100+
94101
```jsx
95102
// ProductPage.jsx -- Server Component (registered with registerServerComponent)
96103
import Providers from './providers';
97104
import Header from './components/Header';
98105
import Footer from './components/Footer';
99106
import ProductDetails from './components/ProductDetails';
100107

101-
export default async function ProductPage({ user, productId }) {
102-
const product = await getProduct(productId);
103-
108+
export default function ProductPage({ user, product }) {
104109
return (
105110
<div>
106111
<Header /> {/* Server Component -- outside providers */}
@@ -222,14 +227,12 @@ export default function ReduxProvider({ children }) {
222227
```
223228

224229
```jsx
225-
// ProductPage.jsx -- Server Component (migrated)
230+
// ProductPage.jsx -- Server Component (migrated, receives product as Rails prop)
226231
import ReduxProvider from './ReduxProvider';
227232
import ProductSpecs from './ProductSpecs';
228233
import AddToCartButton from './AddToCartButton';
229234

230-
export default async function ProductPage({ productId }) {
231-
const product = await getProduct(productId);
232-
235+
export default function ProductPage({ product }) {
233236
return (
234237
<ReduxProvider>
235238
<h1>{product.name}</h1> {/* Server-rendered */}

0 commit comments

Comments
 (0)