Add RSC migration guide series (#2459)#2460
Conversation
Five-part guide covering how to restructure an existing React app for React Server Components: component tree patterns, context/state management, data fetching migration, third-party library compatibility, and troubleshooting. Focuses on the React-side challenges rather than framework configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds six new migration guides under Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryAdds a comprehensive 5-part RSC migration guide series (2,564 new lines) focusing on React-side restructuring challenges rather than framework configuration. Content Overview:
Quality:
Documentation-only change: No code, config, or test modifications. Safe to merge. Confidence Score: 5/5
Important Files Changed
Last reviewed commit: 438c99f |
PR Review: RSC Migration Guide SeriesThis is a substantial documentation contribution that covers React Server Components migration comprehensively. The structure is logical, the patterns are technically sound, and the cross-referencing between articles is consistent. Below are issues ranging from code bugs to scope concerns. Bug: Wrong import in error boundary exampleIn import { useRouter, startTransition } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { startTransition } from 'react';Concern: Serialization table may be inaccurate for React 19In Bug:
|
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; |
There was a problem hiding this comment.
startTransition is not exported from next/navigation — it comes from react. This code will throw at runtime.
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; |
| |---------|-------------| | ||
| | Strings, numbers, booleans, `null`, `undefined` | Functions (except Server Actions) | | ||
| | Plain objects and arrays | Class instances | | ||
| | `Date` objects | `Map`, `Set`, `WeakMap`, `WeakSet` | |
There was a problem hiding this comment.
This row may be inaccurate for React 19. The Flight protocol in React 19 added native serialization support for Map, Set, and typed arrays (e.g. Uint8Array). Please verify against the React 19 changelog and the Flight source, then either correct the table or add a version qualifier like "(React 18 only)".
| value={quantity} | ||
| onChange={(e) => setQuantity(Number(e.target.value))} | ||
| /> | ||
| <button onClick={() => addToCart(productId, quantity)}> |
There was a problem hiding this comment.
addToCart is called here but is never imported or defined in this file. The "After" example is presented as a new, self-contained Client Component, so readers will be puzzled about where this function comes from. Consider adding an import showing the intended pattern — e.g. a Server Action:
// AddToCartButton.jsx -- Client Component (interactive leaf)
'use client';
import { useState } from 'react';
import { addToCart } from '../actions'; // Server Action for mutation
export default function AddToCartButton({ productId }) {| export const UserContext = createContext(null); | ||
|
|
||
| export default function UserProvider({ children, userPromise }) { | ||
| return <UserContext value={userPromise}>{children}</UserContext>; |
There was a problem hiding this comment.
This is the React 19 shorthand context syntax. React 18 users need <UserContext.Provider value={userPromise}>. Since readers may not be on React 19 despite the prerequisites listing it, a brief comment would prevent confusion:
| return <UserContext value={userPromise}>{children}</UserContext>; | |
| return <UserContext value={userPromise}>{children}</UserContext>; // React 19+ shorthand; use <UserContext.Provider value={...}> in React 18 |
|
|
||
| useEffect(() => setMounted(true), []); | ||
|
|
||
| if (!mounted) return null; // Server render returns null |
There was a problem hiding this comment.
The if (!mounted) return null pattern causes Cumulative Layout Shift (CLS) — the element occupies no space on first paint, then pops in after hydration. It's acceptable for small isolated controls (like a single toggle button), but should not be used for any content that affects layout. Consider adding a caveat:
Note: Returning
nullbefore mount causes layout shift. Only use this pattern for small, positionally stable UI elements (icon buttons, toggles). For anything that affects page layout, read the preference from a server-readable cookie (see the Theme Provider section above) to render the correct value on first paint.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/migrating/migrating-to-rsc.md`:
- Around line 1-110: Add the new RSC migration articles to the docs navigation
by editing docs/README.md: under the "🏗️ **Migrating from Other Solutions**"
section, add links to migrating-to-rsc.md and the related pages
(rsc-component-patterns.md, rsc-context-and-state.md, rsc-data-fetching.md,
rsc-third-party-libs.md, rsc-troubleshooting.md) so they appear alongside the
existing migration entries like "From react-rails" and "From Webpack to Rspack";
ensure each entry uses the same markdown list style and relative link format as
the other migration guides.
In `@docs/migrating/rsc-component-patterns.md`:
- Around line 36-54: The fenced code block containing the tree diagram (starting
with "App ('use client')" and the Before/After layout) lacks a language
specifier, causing MD040; update the opening fence from ``` to ```text (or
another appropriate language) so the block is explicitly marked (e.g., change
the diagram's opening ``` to ```text) to satisfy the markdownlint rule.
In `@docs/migrating/rsc-context-and-state.md`:
- Around line 366-415: Summary: The examples use Next.js-specific APIs (import {
cookies } from 'next/headers' and await cookies()) without calling that out. Add
a brief "Note (Next.js):" callout above both examples (the Auth Provider Layout
using verifyToken and AuthProvider, and the Theme Provider Layout reading theme)
stating that cookies() and the async await cookies() API are Next.js-specific
(Next.js 15+), that React on Rails reads request cookies differently, and link
or point readers to the React on Rails Pro docs for the equivalent approach;
keep the note concise and model its style on the existing callout in
migrating-to-rsc.md.
- Around line 183-194: The docs mix React 18 explicit provider syntax
(ThemeContext.Provider) and the React 19 shorthand (UserContext value={...}),
which is confusing; pick one approach and make it consistent: either update the
UserProvider example to use the explicit UserContext.Provider with a value prop
(referencing UserProvider and UserContext) to match ThemeContext.Provider, or
convert ThemeContext.Provider usages to the React 19 shorthand and add a brief
note explaining that <Context value={...}> is the React 19 shorthand; ensure all
examples and the surrounding text consistently reference the chosen style.
In `@docs/migrating/rsc-data-fetching.md`:
- Around line 494-522: The example uses Next.js-specific API revalidatePath
imported from 'next/cache' inside the createComment server action (actions.js)
but doesn't warn readers that this is Next.js-only; add a short disclaimer above
or beside the code block stating that the snippet requires Next.js Server
Actions and revalidatePath from 'next/cache' and link or suggest alternative
APIs (e.g., React on Rails Pro streaming/revalidation) for non-Next.js
environments so users of CommentForm and createComment know this won’t work
outside Next.js.
- Around line 468-480: The examples show improper use of await inside non-async
functions; change both Profile definitions to be async so the await is valid and
the cache-placement lesson is preserved: mark the component function Profile as
async in both examples, leaving the module-level const getUser =
cache(fetchUser) and the in-component const getUser = cache(fetchUser) variants
intact so readers can see the difference in cache placement while ensuring
getUser, cache, fetchUser, and Profile compile correctly.
In `@docs/migrating/rsc-troubleshooting.md`:
- Around line 87-93: The two fenced code blocks that render the import-chain
diagram starting with "ClientComponent.jsx ('use client')" and the
testing-strategy diagram are missing language specifiers (MD040); update each
opening fence from ``` to a fenced block with a language identifier such as
```text (or ```bash/```text if you prefer) so the blocks become ```text ... ```;
locate the blocks by the visible strings "ClientComponent.jsx ('use client')"
and the "testing-strategy" diagram and add the language identifier to each
opening fence.
- Around line 216-247: The example imports startTransition from the wrong
module; change the imports so startTransition comes from 'react' while useRouter
remains imported from 'next/navigation' (update the import statement(s) that
reference startTransition and useRouter), and ensure the retry function in
ErrorFallback still calls startTransition(() => { router.refresh();
resetErrorBoundary(); });; also add a brief note near
PageErrorBoundary/ErrorFallback clarifying that useRouter and router.refresh()
are Next.js-specific APIs so readers using other frameworks (e.g., React on
Rails) won’t be confused.
| # Migrating Your React App to React Server Components | ||
|
|
||
| This guide covers the React-side challenges of migrating an existing React 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. | ||
|
|
||
| > **Note:** For React on Rails-specific configuration (enabling RSC support, Webpack setup, node renderer, view helpers), see the [React on Rails Pro RSC documentation](https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/tutorial/) and the [performance breakthroughs guide](../pro/major-performance-breakthroughs-upgrade-guide.md). | ||
|
|
||
| ## Why Migrate? | ||
|
|
||
| React Server Components offer significant performance benefits when used correctly: | ||
|
|
||
| - **62% reduction** in client-side bundle size ([Frigade case study](https://frigade.com/blog/bundle-size-reduction-with-rsc-and-frigade)) | ||
| - **63% improvement** in Google Speed Index | ||
| - Total blocking time reduced **from 110ms to 1ms** | ||
| - 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. 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. [Component Tree Restructuring Patterns](rsc-component-patterns.md) | ||
|
|
||
| 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 | ||
|
|
||
| ### 2. [Context, Providers, and State Management](rsc-context-and-state.md) | ||
|
|
||
| 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, Zustand, and Jotai to work alongside RSC | ||
| - Using `React.cache()` as a server-side alternative to Context | ||
| - Theme, auth, and i18n provider patterns | ||
|
|
||
| ### 3. [Data Fetching Migration](rsc-data-fetching.md) | ||
|
|
||
| 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 | ||
|
|
||
| ### 4. [Third-Party Library Compatibility](rsc-third-party-libs.md) | ||
|
|
||
| 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 `optimizePackageImports` | ||
| - Using `server-only` and `client-only` packages | ||
|
|
||
| ### 5. [Troubleshooting and Common Pitfalls](rsc-troubleshooting.md) | ||
|
|
||
| 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 | ||
|
|
||
| ## Quick-Start Migration Strategy | ||
|
|
||
| If you want the shortest path to RSC benefits, follow this strategy from [Mux's migration of 50,000 lines](https://www.mux.com/blog/what-are-react-server-components): | ||
|
|
||
| 1. **Add `'use client'` to your app entry point** -- everything works as before, nothing breaks | ||
| 2. **Progressively push the directive lower** -- move `'use client'` from parent components to child components | ||
| 3. **Adopt advanced patterns** -- add Suspense boundaries, streaming, and server-side data fetching | ||
|
|
||
| This three-phase approach lets you migrate incrementally without ever breaking your app. | ||
|
|
||
| ## Component Audit Checklist | ||
|
|
||
| Before you start, audit your components using this classification: | ||
|
|
||
| | Category | Criteria | Action | | ||
| |----------|----------|--------| | ||
| | **Server-ready** (green) | No hooks, no browser APIs, no event handlers | Remove `'use client'` -- these are Server Components by default | | ||
| | **Refactorable** (yellow) | Mix of data fetching and interactivity | Split into a Server Component (data) + Client Component (interaction) | | ||
| | **Client-only** (red) | Uses `useState`, `useEffect`, event handlers, browser APIs | Keep `'use client'` -- these remain Client Components | | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - React 19+ | ||
| - React on Rails 15+ and React on Rails Pro 4+ (for React on Rails projects) | ||
| - Node.js 20+ | ||
| - Understanding of the [server vs client component mental model](https://react.dev/reference/rsc/server-components) | ||
|
|
||
| ## References | ||
|
|
||
| - [React Server Components RFC](https://react.dev/reference/rsc/server-components) | ||
| - [React `'use client'` directive](https://react.dev/reference/rsc/use-client) | ||
| - [React on Rails Pro RSC tutorial](https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/tutorial/) | ||
| - [React on Rails Pro RSC purpose and benefits](https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/purpose-and-benefits/) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find potential sidebar/navigation config files
echo "=== Searching for sidebar/navigation config files ==="
find docs -type f \( -name "*sidebar*" -o -name "*nav*" -o -name "*index*" \) | head -20
echo ""
echo "=== Looking for JS config files in docs root ==="
fd --type f -e js -e json -e yaml -e yml "." docs/ | grep -E "(sidebar|nav|index|config)" | head -20
echo ""
echo "=== Checking for docusaurus or similar framework config ==="
ls -la docs/ 2>/dev/null | head -20
echo ""
echo "=== Looking in docs/migrating/ ==="
ls -la docs/migrating/ 2>/dev/nullRepository: shakacode/react_on_rails
Length of output: 2508
🏁 Script executed:
#!/bin/bash
# Search for references to the new migration guide files
echo "=== Searching for references to new migration files in docs ==="
rg "rsc-component-patterns|rsc-context-and-state|rsc-data-fetching|rsc-third-party-libs|rsc-troubleshooting" docs/ --type md
echo ""
echo "=== Searching for migrating-to-rsc references ==="
rg "migrating-to-rsc" docs/ --type mdRepository: shakacode/react_on_rails
Length of output: 3900
🏁 Script executed:
#!/bin/bash
# Look for issue `#2459` or related context
echo "=== Searching for issue 2459 ==="
rg "2459" . --type md --type txt 2>/dev/null | head -20
echo ""
echo "=== Checking git commits for issue reference ==="
git log --oneline --all 2>/dev/null | grep -i "2459\|rsc\|migration" | head -20Repository: shakacode/react_on_rails
Length of output: 1687
🏁 Script executed:
#!/bin/bash
# Search for sidebar/nav config in root and common locations
echo "=== Searching for sidebar config in entire repo root ==="
find . -maxdepth 2 -type f \( -name "*sidebar*" -o -name "*nav*" \) 2>/dev/null | grep -v node_modules | grep -v ".git"
echo ""
echo "=== Check docs/README.md ==="
cat docs/README.md 2>/dev/null | head -50
echo ""
echo "=== Search for docusaurus or documentation config ==="
fd --type f "sidebar|config" . --max-depth 2 2>/dev/null | grep -v node_modules | grep -v ".git" | head -20Repository: shakacode/react_on_rails
Length of output: 3164
🏁 Script executed:
#!/bin/bash
# Look for documentation structure and any listing/index
echo "=== Checking for _category_.json or similar index files in docs ==="
find docs -name "_category_*" -o -name "index*" 2>/dev/null
echo ""
echo "=== Check if there's a table of contents or index ==="
find docs -type f -name "*.md" | xargs grep -l "Table of Contents\|table of contents" 2>/dev/null | head -5
echo ""
echo "=== List all markdown files in docs/migrating/ ==="
ls -1 docs/migrating/*.mdRepository: shakacode/react_on_rails
Length of output: 843
🏁 Script executed:
#!/bin/bash
# Search for the actual PR/issue context to understand acceptance criteria
echo "=== Searching for 2459 in any documentation ==="
rg "2459" . --type md --type txt --type json 2>/dev/null
echo ""
echo "=== Look for CHANGELOG or PR description ==="
git log 438c99f9 --format="%B" 2>/dev/null | head -50Repository: shakacode/react_on_rails
Length of output: 588
Add RSC migration series to docs navigation — missing from README.md
The new RSC migration guides (migrating-to-rsc.md, rsc-component-patterns.md, rsc-context-and-state.md, rsc-data-fetching.md, rsc-third-party-libs.md, and rsc-troubleshooting.md) are internally cross-linked but not listed in docs/README.md. They should be added to the "🏗️ Migrating from Other Solutions" section alongside other migration guides like "From react-rails" and "From Webpack to Rspack".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/migrating-to-rsc.md` around lines 1 - 110, Add the new RSC
migration articles to the docs navigation by editing docs/README.md: under the
"🏗️ **Migrating from Other Solutions**" section, add links to
migrating-to-rsc.md and the related pages (rsc-component-patterns.md,
rsc-context-and-state.md, rsc-data-fetching.md, rsc-third-party-libs.md,
rsc-troubleshooting.md) so they appear alongside the existing migration entries
like "From react-rails" and "From Webpack to Rspack"; ensure each entry uses the
same markdown list style and relative link format as the other migration guides.
| ``` | ||
| Before: | ||
| App ('use client') <-- Everything is client-side | ||
| ├── Layout | ||
| │ ├── Header | ||
| │ │ ├── Logo | ||
| │ │ └── SearchBar | ||
| │ ├── Content | ||
| │ └── Footer | ||
|
|
||
| After: | ||
| App <-- Server Component (no directive) | ||
| ├── Layout <-- Server Component | ||
| │ ├── Header <-- Server Component | ||
| │ │ ├── Logo <-- Server Component | ||
| │ │ └── SearchBar ('use client') <-- Only this needs client | ||
| │ ├── Content <-- Server Component | ||
| │ └── Footer <-- Server Component | ||
| ``` |
There was a problem hiding this comment.
Add language identifier to fenced code block (MD040).
The tree-diagram block is missing a language specifier, triggering a markdownlint warning.
🔧 Proposed fix
-```
+```text
Before:
App ('use client') <-- Everything is client-side🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 36-36: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-component-patterns.md` around lines 36 - 54, The fenced
code block containing the tree diagram (starting with "App ('use client')" and
the Before/After layout) lacks a language specifier, causing MD040; update the
opening fence from ``` to ```text (or another appropriate language) so the block
is explicitly marked (e.g., change the diagram's opening ``` to ```text) to
satisfy the markdownlint rule.
| ```jsx | ||
| // user-provider.jsx | ||
| 'use client'; | ||
|
|
||
| import { createContext } from 'react'; | ||
|
|
||
| export const UserContext = createContext(null); | ||
|
|
||
| export default function UserProvider({ children, userPromise }) { | ||
| return <UserContext value={userPromise}>{children}</UserContext>; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Inconsistent Context provider syntax across Pattern 1 and Pattern 4.
Pattern 1 (Line 46) uses <ThemeContext.Provider value={...}> (React 18 syntax), while Pattern 4 (Line 192) uses <UserContext value={userPromise}> (the new React 19 shorthand). Mixing both without explanation is likely to confuse readers who expect consistency. Either standardize on one style or add a brief note explaining that <Context value={...}> is the React 19 shorthand.
🔧 Suggested clarification (option A — standardize on React 19 syntax throughout)
- <ThemeContext.Provider value={{ theme, setTheme }}>
- {children}
- </ThemeContext.Provider>
+ <ThemeContext value={{ theme, setTheme }}>
+ {children}
+ </ThemeContext>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-context-and-state.md` around lines 183 - 194, The docs mix
React 18 explicit provider syntax (ThemeContext.Provider) and the React 19
shorthand (UserContext value={...}), which is confusing; pick one approach and
make it consistent: either update the UserProvider example to use the explicit
UserContext.Provider with a value prop (referencing UserProvider and
UserContext) to match ThemeContext.Provider, or convert ThemeContext.Provider
usages to the React 19 shorthand and add a brief note explaining that <Context
value={...}> is the React 19 shorthand; ensure all examples and the surrounding
text consistently reference the chosen style.
| Read auth state on the server, pass to a Client Component provider: | ||
|
|
||
| ```jsx | ||
| // layout.jsx -- Server Component | ||
| import { cookies } from 'next/headers'; | ||
| import { verifyToken } from './lib/auth'; | ||
| import AuthProvider from './auth-provider'; | ||
|
|
||
| export default async function Layout({ children }) { | ||
| const cookieStore = await cookies(); | ||
| const token = cookieStore.get('session')?.value; | ||
| const user = token ? await verifyToken(token) : null; | ||
|
|
||
| return ( | ||
| <html> | ||
| <body> | ||
| <AuthProvider user={user}>{children}</AuthProvider> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| **Key constraints:** | ||
|
|
||
| - Server Components can read `HttpOnly` cookies that client JavaScript cannot access (security advantage) | ||
| - Cookies are read-only in Server Components -- use Server Actions or middleware to set cookies | ||
| - Session refresh must happen in middleware, before the Server Component renders | ||
|
|
||
| ### Theme Provider (No Flash of Wrong Theme) | ||
|
|
||
| For server-side theme rendering without flicker, store the preference in a cookie: | ||
|
|
||
| ```jsx | ||
| // layout.jsx -- Server Component | ||
| import { cookies } from 'next/headers'; | ||
|
|
||
| export default async function Layout({ children }) { | ||
| const cookieStore = await cookies(); | ||
| const theme = cookieStore.get('theme')?.value || 'light'; | ||
|
|
||
| return ( | ||
| <html className={theme}> | ||
| <body>{children}</body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| The correct CSS class is applied during SSR -- no flash of the wrong theme on initial load. A Client Component can update the cookie via a Server Action when the user toggles themes. |
There was a problem hiding this comment.
cookies() from next/headers and await cookies() are Next.js-specific — add disclaimers.
The Auth Provider and Theme Provider examples both import cookies from 'next/headers', which is a Next.js API. The await cookies() call (Line 375, 405) is a Next.js 15+ async cookies API. React on Rails users accessing request cookies do so through different mechanisms. These sections should carry a > **Note (Next.js):** callout, similar to how the opening note in migrating-to-rsc.md points to React on Rails Pro documentation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-context-and-state.md` around lines 366 - 415, Summary: The
examples use Next.js-specific APIs (import { cookies } from 'next/headers' and
await cookies()) without calling that out. Add a brief "Note (Next.js):" callout
above both examples (the Auth Provider Layout using verifyToken and
AuthProvider, and the Theme Provider Layout reading theme) stating that
cookies() and the async await cookies() API are Next.js-specific (Next.js 15+),
that React on Rails reads request cookies differently, and link or point readers
to the React on Rails Pro docs for the equivalent approach; keep the note
concise and model its style on the existing callout in migrating-to-rsc.md.
| ```jsx | ||
| // WRONG: Creating cache inside a component | ||
| function Profile({ userId }) { | ||
| const getUser = cache(fetchUser); // New cache every render! | ||
| const user = await getUser(userId); | ||
| } | ||
|
|
||
| // CORRECT: Define at module level | ||
| const getUser = cache(fetchUser); | ||
| function Profile({ userId }) { | ||
| const user = await getUser(userId); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Both WRONG and CORRECT examples use await inside non-async functions — syntax error.
Neither Profile function is declared async, yet both use await. This is a syntax error that would cause the examples to fail if copied, and obscures the actual lesson (cache placement vs. async usage).
🐛 Proposed fix
// WRONG: Creating cache inside a component
-function Profile({ userId }) {
+async function Profile({ userId }) {
const getUser = cache(fetchUser); // New cache every render!
const user = await getUser(userId);
}
// CORRECT: Define at module level
const getUser = cache(fetchUser);
-function Profile({ userId }) {
+async function Profile({ userId }) {
const user = await getUser(userId);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-data-fetching.md` around lines 468 - 480, The examples
show improper use of await inside non-async functions; change both Profile
definitions to be async so the await is valid and the cache-placement lesson is
preserved: mark the component function Profile as async in both examples,
leaving the module-level const getUser = cache(fetchUser) and the in-component
const getUser = cache(fetchUser) variants intact so readers can see the
difference in cache placement while ensuring getUser, cache, fetchUser, and
Profile compile correctly.
| ```jsx | ||
| // actions.js | ||
| 'use server'; | ||
|
|
||
| import { revalidatePath } from 'next/cache'; | ||
|
|
||
| export async function createComment(formData) { | ||
| const content = formData.get('content'); | ||
| const postId = formData.get('postId'); | ||
|
|
||
| await db.comments.create({ data: { content, postId } }); | ||
| revalidatePath(`/posts/${postId}`); // Triggers re-render with fresh data | ||
| } | ||
| ``` | ||
|
|
||
| ```jsx | ||
| // CommentForm.jsx -- works without JavaScript (progressive enhancement) | ||
| import { createComment } from './actions'; | ||
|
|
||
| export default function CommentForm({ postId }) { | ||
| return ( | ||
| <form action={createComment}> | ||
| <input type="hidden" name="postId" value={postId} /> | ||
| <textarea name="content" /> | ||
| <button type="submit">Post Comment</button> | ||
| </form> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
revalidatePath is a Next.js-specific API — add a disclaimer.
revalidatePath is imported from 'next/cache' with no indication that this is Next.js-only. React on Rails users will not have this module available. A brief note (e.g., > **Note (Next.js):** The following example uses Next.js Server Actions and \revalidatePath`. For React on Rails, use React on Rails Pro's streaming/revalidation APIs.`) would prevent confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-data-fetching.md` around lines 494 - 522, The example uses
Next.js-specific API revalidatePath imported from 'next/cache' inside the
createComment server action (actions.js) but doesn't warn readers that this is
Next.js-only; add a short disclaimer above or beside the code block stating that
the snippet requires Next.js Server Actions and revalidatePath from 'next/cache'
and link or suggest alternative APIs (e.g., React on Rails Pro
streaming/revalidation) for non-Next.js environments so users of CommentForm and
createComment know this won’t work outside Next.js.
| ``` | ||
| ClientComponent.jsx ('use client') | ||
| ├── imports utils.js → becomes client code | ||
| │ └── imports heavy-lib.js → becomes client code (100KB wasted) | ||
| ├── imports helpers.js → becomes client code | ||
| │ └── imports db-utils.js → becomes client code (SECURITY RISK) | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks (MD040).
Both the import-chain diagram at Line 87 and the testing-strategy diagram at Line 322 are missing language specifiers.
🔧 Proposed fix
-```
+```text
ClientComponent.jsx ('use client')
├── imports utils.js → becomes client code🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 87-87: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-troubleshooting.md` around lines 87 - 93, The two fenced
code blocks that render the import-chain diagram starting with
"ClientComponent.jsx ('use client')" and the testing-strategy diagram are
missing language specifiers (MD040); update each opening fence from ``` to a
fenced block with a language identifier such as ```text (or ```bash/```text if
you prefer) so the blocks become ```text ... ```; locate the blocks by the
visible strings "ClientComponent.jsx ('use client')" and the "testing-strategy"
diagram and add the language identifier to each opening fence.
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; | ||
| import { ErrorBoundary } from 'react-error-boundary'; | ||
|
|
||
| function ErrorFallback({ error, resetErrorBoundary }) { | ||
| const router = useRouter(); | ||
|
|
||
| function retry() { | ||
| startTransition(() => { | ||
| router.refresh(); // Re-renders Server Components on the server | ||
| resetErrorBoundary(); // Resets client error boundary state | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <p>Something went wrong</p> | ||
| <button onClick={retry}>Retry</button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default function PageErrorBoundary({ children }) { | ||
| return ( | ||
| <ErrorBoundary FallbackComponent={ErrorFallback}> | ||
| {children} | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
startTransition import React vs next/navigation documentation
💡 Result:
startTransition is a React API, so it’s imported from react, not from Next.js:
import { startTransition } from "react";React docs show this exact import for startTransition. [1]
next/navigation exports routing utilities like useRouter (App Router) and others, but it does not export startTransition—router hooks are imported like:
import { useRouter } from "next/navigation";(And in the Pages Router it’s next/router.) [2]
Typical Next.js App Router usage together:
import { startTransition } from "react";
import { useRouter } from "next/navigation";
const router = useRouter();
startTransition(() => router.push("/dashboard"));Sources: React startTransition reference [1]; Next.js routing API reference (router import paths) [2].
[1] (react.dev)
[2] (nextjs.org)
Citations:
- 1: https://react.dev/reference/react/startTransition?utm_source=openai
- 2: https://nextjs.org/docs/pages/api-reference/functions/use-router?utm_source=openai
🏁 Script executed:
# Check if the file exists and examine the content at the specified lines
fd rsc-troubleshooting.mdRepository: shakacode/react_on_rails
Length of output: 105
🏁 Script executed:
# If file exists, check the content around lines 216-247
if [ -f docs/migrating/rsc-troubleshooting.md ]; then
sed -n '216,247p' docs/migrating/rsc-troubleshooting.md
fiRepository: shakacode/react_on_rails
Length of output: 792
startTransition must be imported from 'react', not 'next/navigation'.
startTransition is a React API and is not exported by next/navigation. This will cause a runtime error. Separate the imports:
-import { useRouter, startTransition } from 'next/navigation';
+import { useRouter } from 'next/navigation';
+import { startTransition } from 'react';Additionally, useRouter and router.refresh() are Next.js-specific APIs. Consider adding a note to clarify this example is for Next.js, as React on Rails users may encounter confusion.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```jsx | |
| 'use client'; | |
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { ErrorBoundary } from 'react-error-boundary'; | |
| function ErrorFallback({ error, resetErrorBoundary }) { | |
| const router = useRouter(); | |
| function retry() { | |
| startTransition(() => { | |
| router.refresh(); // Re-renders Server Components on the server | |
| resetErrorBoundary(); // Resets client error boundary state | |
| }); | |
| } | |
| return ( | |
| <div> | |
| <p>Something went wrong</p> | |
| <button onClick={retry}>Retry</button> | |
| </div> | |
| ); | |
| } | |
| export default function PageErrorBoundary({ children }) { | |
| return ( | |
| <ErrorBoundary FallbackComponent={ErrorFallback}> | |
| {children} | |
| </ErrorBoundary> | |
| ); | |
| } | |
| ``` |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-troubleshooting.md` around lines 216 - 247, The example
imports startTransition from the wrong module; change the imports so
startTransition comes from 'react' while useRouter remains imported from
'next/navigation' (update the import statement(s) that reference startTransition
and useRouter), and ensure the retry function in ErrorFallback still calls
startTransition(() => { router.refresh(); resetErrorBoundary(); });; also add a
brief note near PageErrorBoundary/ErrorFallback clarifying that useRouter and
router.refresh() are Next.js-specific APIs so readers using other frameworks
(e.g., React on Rails) won’t be confused.
Visual diagram showing the three-phase RSC migration process: Phase 1 (all client) → Phase 2 (push boundary down) → Phase 3 (split mixed components). Uses animated transitions with color-coded server (cyan) and client (orange) components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review: RSC Migration Guide SeriesThis is a comprehensive and well-structured documentation series. The content is accurate in its high-level concepts and the code examples demonstrate real-world patterns effectively. A few issues need addressing before merging. Critical Bug
Code Accuracy Issues"Solution 4: Mixed Strategy" in Inconsistent React Context API in
Security GapServer Actions need an authentication note in Documentation QualityFramework-specific APIs are unlabeled: "Mounted" pattern needs a stronger caveat: Positive Notes
|
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; |
There was a problem hiding this comment.
Bug: startTransition is not exported from next/navigation
startTransition is a React API, not a Next.js API. This import will fail at runtime with a "module not found" or undefined error.
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; |
| <div> | ||
| <ProductDetail product={product} /> | ||
| <Suspense fallback={<ReviewsSkeleton />}> | ||
| <ReviewsSection promise={reviewsPromise} /> |
There was a problem hiding this comment.
Incomplete example: ReviewsSection and RelatedSection are never defined
The example passes promise={reviewsPromise} as a prop but never shows what these components look like or how they consume the promise. This creates ambiguity about the intended pattern:
- If they are Server Components, passing a promise this way adds no value — they should simply call
getReviews(productId)directly inside themselves.React.cache()ensures no duplicate fetch. - If they are Client Components using
use(), they need'use client'and ause(promise)call, which should be shown.
Consider adding a follow-up component definition, e.g.:
// ReviewsSection.jsx -- Client Component
'use client';
import { use } from 'react';
export default function ReviewsSection({ promise }) {
const reviews = use(promise);
return <ReviewList reviews={reviews} />;
}Or, if keeping them as Server Components, remove the promise-passing and call getReviews(productId) directly inside ReviewsSection, which is simpler and equally performant thanks to React.cache().
| export const UserContext = createContext(null); | ||
|
|
||
| export default function UserProvider({ children, userPromise }) { | ||
| return <UserContext value={userPromise}>{children}</UserContext>; |
There was a problem hiding this comment.
Inconsistent Context API syntax in this article
This uses React 19's new shorthand <Context value={...}>, but earlier examples in the same file (line 46) use the older <Context.Provider value={...}> syntax. Both are valid, but mixing them without explanation will confuse readers, particularly those who haven't yet upgraded to React 19.
Since the prerequisites list React 19+, using the new shorthand consistently is fine — but you should either:
- Use the new syntax consistently throughout all examples in the article, or
- Add a brief note the first time it appears:
// React 19+ shorthand for <UserContext.Provider value={...}>
|
|
||
| **Error:** `"'App' cannot be used as a JSX component. Its return type 'Promise<JSX.Element>' is not a valid JSX element type."` | ||
|
|
||
| **Fix:** Upgrade to TypeScript 5.1.2+ with `@types/react` 18.2.8+, or omit the explicit return type annotation: |
There was a problem hiding this comment.
Outdated @types/react version for a React 19+ guide
@types/react 18.2.8 introduced async component support for React 18, but this guide's prerequisites require React 19+. For React 19 projects the correct package is @types/react@19 (or @types/react@^19). Recommending an 18.x version here may cause readers to install the wrong type package.
| **Fix:** Upgrade to TypeScript 5.1.2+ with `@types/react` 18.2.8+, or omit the explicit return type annotation: | |
| **Fix:** Upgrade to TypeScript 5.1.2+ with `@types/react@19` (React 19 projects) or `@types/react` 18.2.8+ (React 18 projects), or omit the explicit return type annotation: |
|
|
||
| import { revalidatePath } from 'next/cache'; | ||
|
|
||
| export async function createComment(formData) { |
There was a problem hiding this comment.
Missing security note: Server Actions are public HTTP endpoints
Server Actions are exposed as POST endpoints that anyone can call — they are not restricted to your own UI. This example has no authentication or authorization check. A reader who copies this directly will have an unauthenticated mutation endpoint.
Consider adding a comment and note:
export async function createComment(formData) {
// Server Actions are public endpoints — always authenticate
const session = await getSession();
if (!session?.userId) throw new Error('Unauthorized');
const content = formData.get('content');
// ...
}And add a callout after the example: Security: Always verify the caller is authenticated and authorized before performing mutations. Never trust formData values without validation — see the runtime validation example in the Troubleshooting guide.
|
|
||
| useEffect(() => setMounted(true), []); | ||
|
|
||
| if (!mounted) return null; // Server render returns null |
There was a problem hiding this comment.
Caveat needed: this pattern harms Core Web Vitals
if (!mounted) return null means the server renders nothing for this element, then the client renders actual content after hydration. This causes:
- CLS (Cumulative Layout Shift) — content pops in after the initial paint, which Google measures and penalizes
- Flash of missing content — visible to users on slower connections
This should be presented as a last resort with a clear warning. A better fix is usually architectural: either read the value from a cookie on the server (to avoid the mismatch entirely), or accept a controlled mismatch with suppressHydrationWarning on a non-critical element.
Consider adding: // Warning: causes CLS. Prefer reading from cookies on the server, or use suppressHydrationWarning for non-layout-critical elements.
| 2. **Tree-shaking failure:** Bundlers struggle to eliminate unused exports | ||
| 3. **Mixed server/client exports:** Known bugs in Next.js when a barrel file exports both server and client components | ||
|
|
||
| ### The Solution: `optimizePackageImports` |
There was a problem hiding this comment.
optimizePackageImports is a Next.js-only feature
This configuration option is specific to Next.js (next.config.js) and is not available in other RSC-supporting frameworks (Waku, Parcel RSC, etc.). Since the guide aims to cover React-side challenges generally, this section should be labeled as a Next.js-specific solution.
Consider prefixing the heading or adding a note: Next.js only: optimizePackageImports is a Next.js-specific feature. Other frameworks may handle barrel file optimization differently (e.g., via bundler plugins or sideEffects: false in package.json).
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/migrating/rsc-component-patterns.md`:
- Around line 108-122: The "After" example dropped the module export for the
server component—restore the export so consumers don't break: add the missing
export for ProductPage (e.g., export default async function ProductPage(...) or
export default ProductPage at the end) so the function ProductPage remains the
default export and matches the "Before" example.
- Around line 276-307: The snippet is missing imports and the Stats component
isn't exported; update Dashboard to import Stats, RevenueChart, RecentOrders,
StatsSkeleton, ChartSkeleton, and TableSkeleton (so Dashboard can render those
symbols), and make the Stats component an exported module (export default Stats
or export a named Stats and ensure Dashboard imports it accordingly) so the
runtime import resolves; adjust import/export consistently between Dashboard and
Stats.
- Around line 336-352: Add a React version note before the Comments component
showing that use(commentsPromise) requires React 19; update the docs around the
Comments component (the Comments function and its use(commentsPromise) call) to
include a short callout that this pattern depends on React 19 (React 18 lacks
use()) so readers on React 18 will encounter errors. Keep the callout concise
and place it immediately above the code block that begins with "'use client';"
and the Comments component.
---
Duplicate comments:
In `@docs/migrating/rsc-component-patterns.md`:
- Around line 40-58: The fenced code block containing the Before/After tree
diagram (the block starting with ``` and showing App, Layout, Header, Logo,
SearchBar, Content, Footer) is missing a language specifier; add an appropriate
specifier such as ```text (or ```diff if preferred) to the opening fence so the
diagram is rendered with correct formatting and syntax highlighting.
| ```jsx | ||
| // ProductPage.jsx -- Server Component (no directive) | ||
| async function ProductPage({ productId }) { | ||
| const product = await getProduct(productId); | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>{product.name}</h1> | ||
| <p>{product.description}</p> | ||
| <ProductSpecs specs={product.specs} /> | ||
| <ReviewList reviews={product.reviews} /> | ||
| <AddToCartButton productId={product.id} /> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
export default missing from ProductPage in the "After" example.
The "Before" snippet (line 73) has export default function ProductPage, but the "After" snippet drops the export, creating an inconsistency. Readers migrating their own code might incorrectly remove the export during the refactor.
📝 Proposed fix
-async function ProductPage({ productId }) {
+export default async function ProductPage({ productId }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-component-patterns.md` around lines 108 - 122, The "After"
example dropped the module export for the server component—restore the export so
consumers don't break: add the missing export for ProductPage (e.g., export
default async function ProductPage(...) or export default ProductPage at the
end) so the function ProductPage remains the default export and matches the
"Before" example.
| ```jsx | ||
| // Dashboard.jsx -- Server Component | ||
| import { Suspense } from 'react'; | ||
|
|
||
| export default function Dashboard() { | ||
| return ( | ||
| <div> | ||
| <h1>Dashboard</h1> | ||
| <Suspense fallback={<StatsSkeleton />}> | ||
| <Stats /> {/* Fetches and renders independently */} | ||
| </Suspense> | ||
| <Suspense fallback={<ChartSkeleton />}> | ||
| <RevenueChart /> {/* Fetches and renders independently */} | ||
| </Suspense> | ||
| <Suspense fallback={<TableSkeleton />}> | ||
| <RecentOrders /> {/* Fetches and renders independently */} | ||
| </Suspense> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // Stats.jsx -- Async Server Component | ||
| async function Stats() { | ||
| const stats = await getStats(); // Direct server-side fetch | ||
| return ( | ||
| <div> | ||
| <span>Revenue: {stats.revenue}</span> | ||
| <span>Users: {stats.users}</span> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Pattern 4 code example has two correctness issues: missing imports and a missing export.
Dashboardrenders<Stats />,<RevenueChart />,<RecentOrders />,<StatsSkeleton />,<ChartSkeleton />, and<TableSkeleton />but imports none of them. Readers copying the snippet will get "X is not defined" errors.Statsis defined asasync function Stats()(unexported named function), soDashboard's (implied)import Stats from './Stats'would fail at runtime — it must be a default or named export.
📝 Proposed fix
// Dashboard.jsx -- Server Component
import { Suspense } from 'react';
+import Stats from './Stats';
+import RevenueChart from './RevenueChart';
+import RecentOrders from './RecentOrders';
+import { StatsSkeleton, ChartSkeleton, TableSkeleton } from './Skeletons';
export default function Dashboard() { // Stats.jsx -- Async Server Component
-async function Stats() {
+export default async function Stats() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-component-patterns.md` around lines 276 - 307, The snippet
is missing imports and the Stats component isn't exported; update Dashboard to
import Stats, RevenueChart, RecentOrders, StatsSkeleton, ChartSkeleton, and
TableSkeleton (so Dashboard can render those symbols), and make the Stats
component an exported module (export default Stats or export a named Stats and
ensure Dashboard imports it accordingly) so the runtime import resolves; adjust
import/export consistently between Dashboard and Stats.
| ```jsx | ||
| // Comments.jsx -- Client Component | ||
| 'use client'; | ||
|
|
||
| import { use } from 'react'; | ||
|
|
||
| export default function Comments({ commentsPromise }) { | ||
| const comments = use(commentsPromise); // Resolves the promise | ||
| return ( | ||
| <ul> | ||
| {comments.map(c => <li key={c.id}>{c.text}</li>)} | ||
| </ul> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| **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. |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
React 18 vs React 19 use() hook promise support stability
💡 Result:
React 18 vs React 19 for use() + Promise support + stability
React 18 (incl. 18.2 / 18.3)
- No
use()API for consuming Promises/Context in render (it’s a React 19 feature). - React published React 18.3 (same behavior as 18.2) mainly to surface warnings for React 19 deprecations/breaking changes, and recommends upgrading to 18.3 first before moving to 19. [3]
React 19
- Stable release: React 19 is stable as of December 5, 2024 (official React team post; date updated to reflect stable release). [1]
- What
use(promise)does:use(Promise)lets a component suspend during render until the Promise resolves, integrating with<Suspense>fallbacks and Error Boundaries for rejected Promises. [2] - Key limitation (important for “promise support stability”):
usedoes not support Promises created in render (React warns about “uncached promise”). You’re expected to pass a cached/stable Promise (commonly: created outside the client render path—often from a Server Component or a Suspense-aware library/framework). [1][2]- React docs explicitly note that Promises created in Client Components are recreated on every render, while Promises passed from Server Components to Client Components are stable across re-renders. [2]
Practical takeaway
- If you want stable, supported
use(Promise)behavior, React 19 is the correct baseline—but you must structure code so the Promise is stable/cached, not created inline during client render. [1][2] - If you’re still on React 18, go to 18.3 first to catch upgrade warnings, then move to 19. [3]
Sources: React v19 release post [1], official use API reference [2], React 19 upgrade guide (incl. React 18.3 recommendation) [3].
Add a React 19 version requirement callout for the use(promise) pattern.
Pattern 5 uses use(promise) to resolve a server-sent Promise on the client—this API is only available in React 19. React 18 does not have a use() hook and readers using React 18 will encounter errors if they copy this pattern.
Add a version note before the code block:
+> **Requires React 19+.** The `use(promise)` pattern for server-to-client promise handoff is not available in React 18.
+
```jsx
// Comments.jsx -- Client Component
'use client';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-component-patterns.md` around lines 336 - 352, Add a React
version note before the Comments component showing that use(commentsPromise)
requires React 19; update the docs around the Comments component (the Comments
function and its use(commentsPromise) call) to include a short callout that this
pattern depends on React 19 (React 18 lacks use()) so readers on React 18 will
encounter errors. Keep the callout concise and place it immediately above the
code block that begins with "'use client';" and the Comments component.
- All three trees now show ProductPage (not Content) - Phase 2: SearchBar + ProductPage are client (~40% bundle) - Phase 3: ProductPage converts to server, only AddCart is client (~15%) - Phase 2 bundle bar is visibly larger than Phase 3 - Orange-to-cyan stroke animation on ProductPage in Phase 3 highlights conversion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; | ||
| import { ErrorBoundary } from 'react-error-boundary'; |
There was a problem hiding this comment.
Bug: startTransition is not exported from next/navigation
startTransition is a React API and must be imported from react, not next/navigation. This import will fail at runtime (the value will be undefined, and calling undefined() throws a TypeError when the user clicks "Retry").
| import { ErrorBoundary } from 'react-error-boundary'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; | |
| import { ErrorBoundary } from 'react-error-boundary'; |
|
|
||
| ```jsx | ||
| // Post.jsx -- Server Component | ||
| import { preloadComments, getComments } from '../lib/data'; |
There was a problem hiding this comment.
Missing import: getPost is used but not imported
getPost(postId) is called on the next line, but only preloadComments and getComments are imported from '../lib/data'. Readers following along will get a ReferenceError.
| import { preloadComments, getComments } from '../lib/data'; | |
| import { preloadComments, getComments, getPost } from '../lib/data'; |
|
|
||
| export default function UserProvider({ children, userPromise }) { | ||
| return <UserContext value={userPromise}>{children}</UserContext>; | ||
| } |
There was a problem hiding this comment.
Context API syntax inconsistency with the guide's own React 19 requirement
Pattern 1 (ThemeProvider, line 46) uses <ThemeContext.Provider value={...}> (the pre-React 19 syntax), while Pattern 4 here uses <UserContext value={userPromise}> (the React 19 "value as prop" shorthand).
Since migrating-to-rsc.md lists React 19+ as a prerequisite, both syntaxes are technically valid, but mixing them in the same guide file without explanation will confuse readers. Either standardise on one form, or add a note explaining that React 19 allows the shorter form.
| import AuthProvider from './auth-provider'; | ||
|
|
||
| export default async function Layout({ children }) { | ||
| const cookieStore = await cookies(); |
There was a problem hiding this comment.
These examples are Next.js-specific, not React on Rails patterns
cookies() from next/headers, verifyToken() from middleware, revalidatePath(), await cookies() — these are all Next.js App Router APIs that don't exist in a React on Rails project. Readers following this guide for a React on Rails migration will hit immediate errors trying to replicate these patterns.
Consider either:
- Adding a clear callout box like
> **Note:** The following examples use Next.js App Router APIs. For React on Rails, … - Replacing with framework-agnostic equivalents (e.g., reading cookies from a plain HTTP library, or pointing to the React on Rails Pro auth documentation)
docs/migrating/rsc-data-fetching.md
Outdated
| // actions.js | ||
| 'use server'; | ||
|
|
||
| import { revalidatePath } from 'next/cache'; |
There was a problem hiding this comment.
Next.js-specific API without framing
revalidatePath is a Next.js API. Likewise, the import { revalidatePath } from 'next/cache' line a few lines above will fail in any non-Next.js RSC environment (including React on Rails Pro). The same applies to HydrationBoundary from @tanstack/react-query and dehydrate — these work in Next.js but the dehydration/hydration approach may differ in other frameworks.
A short callout noting "This example targets Next.js App Router. For React on Rails, see the Pro RSC docs" would make this much clearer for the target audience.
docs/migrating/rsc-data-fetching.md
Outdated
| // UserProfile.jsx -- Server Component (no directive) | ||
|
|
||
| export default async function UserProfile({ userId }) { | ||
| const user = await fetch(`https://api.example.com/users/${userId}`) |
There was a problem hiding this comment.
Fetch to absolute URL silently fails in many server environments
The "Before" example fetches from a relative URL (/api/users/${userId}), while the "After" example switches to an absolute URL (https://api.example.com/...). The change in base URL is intentional and correct (Node.js fetch requires absolute URLs), but it's not called out in the "What changed" list below. A reader may replicate the relative URL from their existing code and get a silent runtime failure.
Worth adding a bullet: "- Used an absolute URL (required in server-side fetch)"
| | **CSS Modules** | Built into most frameworks. No runtime overhead. | | ||
| | **Panda CSS** | Zero-runtime, type-safe, created by Chakra UI team. RSC-compatible by design. | | ||
| | **Pigment CSS** | Created by MUI. Compiles to CSS Modules. Still unstable as of early 2026. | | ||
| | **vanilla-extract** | TypeScript-native. Known issue: `.css.ts` imports in RSC may need `swc-plugin-vanilla-extract` workaround. | |
There was a problem hiding this comment.
Time-sensitive claim that will become stale
"Still unstable as of early 2026" will be incorrect once Pigment CSS reaches stability. Consider replacing with a link to the library's release notes or stability page so readers always get current status rather than a snapshot that grows outdated.
| </time> | ||
| ``` | ||
|
|
||
| This suppresses the warning but does not fix the mismatch -- use it only for non-critical content. |
There was a problem hiding this comment.
suppressHydrationWarning only suppresses one level deep
The text says "This suppresses the warning but does not fix the mismatch", which is correct, but is missing an important constraint: suppressHydrationWarning only suppresses warnings on the element itself, not on its children. Readers using it on a wrapper element expecting it to silence nested mismatches will still see errors. Worth adding that note here.
| } | ||
| ``` | ||
|
|
||
| ### Runtime Validation for Server Actions |
There was a problem hiding this comment.
Security: Server Actions are public endpoints note deserves higher prominence
This is the most security-critical point in the entire guide series. A TypeScript type signature on a Server Action provides zero protection at runtime — any HTTP client can POST to it with arbitrary data. This single paragraph buries a critical security rule that deserves its own dedicated callout block (e.g., a > ⚠️ Security blockquote) and should also be mentioned in rsc-data-fetching.md when Server Actions are introduced there.
| For third-party packages, Next.js can automatically transform barrel imports into direct imports: | ||
|
|
||
| ```js | ||
| // next.config.js |
There was a problem hiding this comment.
optimizePackageImports is a Next.js-specific config key.
This setting only works with Next.js. The series introduction notes it covers "React-side challenges" and links to React on Rails Pro for framework config, but this section drops into Next.js-specific next.config.js config without any qualifier.
React on Rails users using Webpack/Rspack would need a different approach (e.g., sideEffects: false in package.json, or explicit direct imports). Worth adding a note like:
Note:
optimizePackageImportsis a Next.js feature. For React on Rails (Webpack/Rspack), avoid barrel-file imports by using direct module paths, or configure your bundler's tree-shaking for the package.
Review: RSC Migration Guide SeriesThis is a well-researched and comprehensive documentation series. The structure is logical, the before/after code examples are genuinely useful, and the depth on non-obvious topics (chunk contamination in Mistake 3, Bugs in Code Examples1. Wrong import source for
2. The API Inconsistency3. Mixed React Context syntax in the same guide (rsc-context-and-state.md) Pattern 1 uses Accuracy4. RSC payload description is imprecise (rsc-troubleshooting.md:312) Describing it as content "sent twice" misrepresents the format. The RSC payload (React Flight wire format) is structurally different from the initial HTML — it's a compact serialized component tree for client-side hydration, not a duplicate of the HTML. See inline comment for suggested rewording. Framework Scope5. Next.js-specific APIs used throughout without qualification Numerous examples rely on APIs that are Next.js-only:
The intro correctly scopes to React-side concerns and links out to React on Rails Pro for framework config. But sections like the Server Actions mutation example and the barrel file solution silently assume Next.js. React on Rails users on Webpack/Rspack need different guidance for some of these. A callout at the top of affected sections (similar to what the intro does) would prevent confusion. Security6. Server Action mutation example has no input validation (rsc-data-fetching.md:589) The |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (8)
docs/migrating/rsc-component-patterns.md (4)
108-122:⚠️ Potential issue | 🟡 MinorUnresolved:
ProductPagestill missingexport defaultin the "After" snippet.The "Before" snippet at Line 73 has
export default function ProductPage, but the "After" block at Line 110 drops the export. Inconsistency persists from the previous review round.📝 Proposed fix
-async function ProductPage({ productId }) { +export default async function ProductPage({ productId }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 108 - 122, The ProductPage "After" example is missing the export; update the exported server component declaration so it matches the "Before" snippet by making the ProductPage function the default export (i.e., restore the export default on the ProductPage symbol), ensuring the documented "After" snippet shows export default ProductPage to keep examples consistent.
40-58:⚠️ Potential issue | 🟡 MinorUnresolved: fenced code block still missing a language specifier (MD040).
The tree diagram block at Line 40 has no language identifier, which triggers a markdownlint MD040 warning. This was flagged in a previous review round and is still unaddressed.
🔧 Proposed fix
-``` +```text Before: App ('use client') <-- Everything is client-side🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 40 - 58, The fenced code block showing the component tree (the block that begins with the triple backticks and lines like "App ('use client') <-- Everything is client-side") lacks a language specifier and triggers MD040; fix it by adding a language identifier (e.g., "text") right after the opening ``` so the block becomes ```text, leaving the tree contents unchanged, ensuring markdownlint stops warning about the missing language.
336-352:⚠️ Potential issue | 🟡 MinorUnresolved: no React 19 version callout before the
use(commentsPromise)example.
use(promise)is a React 19-only API. Readers on React 18 will hit a runtime error. A version note was requested in the prior review but has not been added.📝 Proposed fix
+> **Requires React 19+.** The `use(promise)` pattern for server-to-client promise handoff is not available in React 18. + ```jsx // Comments.jsx -- Client Component 'use client';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 336 - 352, The example uses the React 19-only API use(commentsPromise) inside the Comments component, which will error on React 18; add a clear React version callout immediately before the Comments.jsx example stating that use(promise) requires React 19+ and will not work in React 18, and suggest the React 18 alternative (e.g., useEffect + useState to resolve comments or using server-side Suspense patterns) so readers know how to adapt the Comments component (look for the Comments function and the use(commentsPromise) call to insert the note).
276-307:⚠️ Potential issue | 🟡 MinorUnresolved: Pattern 4 still missing imports in
Dashboardandexport defaultonStats.
Dashboardrenders<Stats />,<RevenueChart />,<RecentOrders />,<StatsSkeleton />,<ChartSkeleton />, and<TableSkeleton />without importing any of them.Stats(Line 298) is also not exported. Both issues were flagged in the prior review and remain.📝 Proposed fix
// Dashboard.jsx -- Server Component import { Suspense } from 'react'; +import Stats from './Stats'; +import RevenueChart from './RevenueChart'; +import RecentOrders from './RecentOrders'; +import { StatsSkeleton, ChartSkeleton, TableSkeleton } from './Skeletons';-async function Stats() { +export default async function Stats() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 276 - 307, Dashboard is missing imports for the child components it renders and Stats is not exported; add import statements for Stats, RevenueChart, RecentOrders, StatsSkeleton, ChartSkeleton, and TableSkeleton at the top of the file and export the Stats component (e.g., export default or named export consistent with Dashboard's usage) so that <Dashboard /> can render <Stats />, <RevenueChart />, and <RecentOrders /> and their skeletons without unresolved references.docs/migrating/rsc-troubleshooting.md (4)
216-247:⚠️ Potential issue | 🔴 CriticalUnresolved critical:
startTransitionis still incorrectly imported from'next/navigation'.
startTransitionis a React core API and is not exported bynext/navigation. This import will cause a runtime error for anyone copying the snippet. This was flagged as critical in the previous review.🐛 Proposed fix
-import { useRouter, startTransition } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import { startTransition } from 'react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 216 - 247, The snippet incorrectly imports startTransition from 'next/navigation' causing runtime errors; update the imports so startTransition is imported from React and useRouter remains from 'next/navigation' (i.e., remove startTransition from the next/navigation import and add import { startTransition } from 'react'), then ensure the ErrorFallback component (and its retry function which calls startTransition and router.refresh) continues to use those corrected imports so retry and PageErrorBoundary work as intended.
87-93:⚠️ Potential issue | 🟡 MinorUnresolved: import-chain diagram block still missing a language specifier (MD040).
The fenced code block at Line 87 has no language identifier. This was flagged in the previous review and remains.
🔧 Proposed fix
-``` +```text ClientComponent.jsx ('use client') ├── imports utils.js → becomes client code🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 87 - 93, The fenced code block showing the import-chain diagram (starting with "ClientComponent.jsx ('use client')") is missing a language identifier; update the block delimiter from ``` to ```text so the block is explicitly marked as plain text (e.g., replace the opening ``` with ```text for the diagram block containing "ClientComponent.jsx ('use client')" and its tree lines) to satisfy MD040.
322-338:⚠️ Potential issue | 🟡 MinorUnresolved: testing-strategy diagram block still missing a language specifier (MD040).
The fenced code block at Line 322 has no language identifier. This was flagged in the previous review and remains.
🔧 Proposed fix
-``` +```text Unit Tests (Vitest/Jest) ├── Client Components -- full support with hooks mocking🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 322 - 338, The fenced diagram block starting with "Unit Tests (Vitest/Jest)" lacks a language specifier; update the opening fence from ``` to ```text (or another appropriate language like ```md) so the block becomes a fenced code block with a language identifier (e.g., change the fence that precedes "Unit Tests (Vitest/Jest)" to ```text).
465-470:⚠️ Potential issue | 🟡 MinorUnresolved:
fetch(process.env.DATABASE_URL)is still a misleading example.
DATABASE_URLis a database connection string (e.g.,postgresql://user:pass@host/db), not an HTTP URL.fetch()will throw on non-HTTP schemes, so the// Workscomment is incorrect. This was flagged in the previous review.📝 Proposed fix
async function DBComponent() { - const data = await fetch(process.env.DATABASE_URL); // Works - const secret = process.env.API_SECRET; // Works + // Full access to all env vars — DB credentials, secrets, internal URLs + const db = new DatabaseClient(process.env.DATABASE_URL); // Works + const secret = process.env.API_SECRET; // Works }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 465 - 470, The example DBComponent is misleading because it calls fetch(process.env.DATABASE_URL) even though DATABASE_URL is a DB connection string; update the example to be correct by either (a) replacing fetch(process.env.DATABASE_URL) with an HTTP fetch using a proper env var (e.g., process.env.API_URL) and keep the "// Works" comment, or (b) illustrate connecting to a database using a DB client (e.g., show creating a client and connecting with process.env.DATABASE_URL) and change the comment accordingly; locate the DBComponent function in the snippet and modify the call and comment so the code matches the intended resource type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/migrating/rsc-component-patterns.md`:
- Line 368: The table cell text contains "markdown" in lowercase; update that
cell to use the proper noun "Markdown" (e.g., change "Heavy dependencies
(markdown parsers, formatters)" to "Heavy dependencies (Markdown parsers,
formatters)") so the term is capitalized correctly in the row that currently
reads "Heavy dependencies (markdown parsers, formatters) | Server | Dependencies
stay off client bundle".
---
Duplicate comments:
In `@docs/migrating/rsc-component-patterns.md`:
- Around line 108-122: The ProductPage "After" example is missing the export;
update the exported server component declaration so it matches the "Before"
snippet by making the ProductPage function the default export (i.e., restore the
export default on the ProductPage symbol), ensuring the documented "After"
snippet shows export default ProductPage to keep examples consistent.
- Around line 40-58: The fenced code block showing the component tree (the block
that begins with the triple backticks and lines like "App ('use client')
<-- Everything is client-side") lacks a language specifier and triggers MD040;
fix it by adding a language identifier (e.g., "text") right after the opening
``` so the block becomes ```text, leaving the tree contents unchanged, ensuring
markdownlint stops warning about the missing language.
- Around line 336-352: The example uses the React 19-only API
use(commentsPromise) inside the Comments component, which will error on React
18; add a clear React version callout immediately before the Comments.jsx
example stating that use(promise) requires React 19+ and will not work in React
18, and suggest the React 18 alternative (e.g., useEffect + useState to resolve
comments or using server-side Suspense patterns) so readers know how to adapt
the Comments component (look for the Comments function and the
use(commentsPromise) call to insert the note).
- Around line 276-307: Dashboard is missing imports for the child components it
renders and Stats is not exported; add import statements for Stats,
RevenueChart, RecentOrders, StatsSkeleton, ChartSkeleton, and TableSkeleton at
the top of the file and export the Stats component (e.g., export default or
named export consistent with Dashboard's usage) so that <Dashboard /> can render
<Stats />, <RevenueChart />, and <RecentOrders /> and their skeletons without
unresolved references.
In `@docs/migrating/rsc-troubleshooting.md`:
- Around line 216-247: The snippet incorrectly imports startTransition from
'next/navigation' causing runtime errors; update the imports so startTransition
is imported from React and useRouter remains from 'next/navigation' (i.e.,
remove startTransition from the next/navigation import and add import {
startTransition } from 'react'), then ensure the ErrorFallback component (and
its retry function which calls startTransition and router.refresh) continues to
use those corrected imports so retry and PageErrorBoundary work as intended.
- Around line 87-93: The fenced code block showing the import-chain diagram
(starting with "ClientComponent.jsx ('use client')") is missing a language
identifier; update the block delimiter from ``` to ```text so the block is
explicitly marked as plain text (e.g., replace the opening ``` with ```text for
the diagram block containing "ClientComponent.jsx ('use client')" and its tree
lines) to satisfy MD040.
- Around line 322-338: The fenced diagram block starting with "Unit Tests
(Vitest/Jest)" lacks a language specifier; update the opening fence from ``` to
```text (or another appropriate language like ```md) so the block becomes a
fenced code block with a language identifier (e.g., change the fence that
precedes "Unit Tests (Vitest/Jest)" to ```text).
- Around line 465-470: The example DBComponent is misleading because it calls
fetch(process.env.DATABASE_URL) even though DATABASE_URL is a DB connection
string; update the example to be correct by either (a) replacing
fetch(process.env.DATABASE_URL) with an HTTP fetch using a proper env var (e.g.,
process.env.API_URL) and keep the "// Works" comment, or (b) illustrate
connecting to a database using a DB client (e.g., show creating a client and
connecting with process.env.DATABASE_URL) and change the comment accordingly;
locate the DBComponent function in the snippet and modify the call and comment
so the code matches the intended resource type.
| | Data fetching (database, API) | Server | Direct backend access, no bundle cost | | ||
| | Rendering static/display-only content | Server | No JavaScript shipped | | ||
| | Using server-only secrets (API keys) | Server | Never exposed to client | | ||
| | Heavy dependencies (markdown parsers, formatters) | Server | Dependencies stay off client bundle | |
There was a problem hiding this comment.
Minor: capitalize "Markdown" as a proper noun.
"markdown" in the table cell should be "Markdown" — it is a proper noun.
📝 Fix
-| Heavy dependencies (markdown parsers, formatters) | Server | Dependencies stay off client bundle |
+| Heavy dependencies (Markdown parsers, formatters) | Server | Dependencies stay off client bundle |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | Heavy dependencies (markdown parsers, formatters) | Server | Dependencies stay off client bundle | | |
| | Heavy dependencies (Markdown parsers, formatters) | Server | Dependencies stay off client bundle | |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~368-~368: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...posed to client | | Heavy dependencies (markdown parsers, formatters) | Server | Depende...
(MARKDOWN_NNP)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/migrating/rsc-component-patterns.md` at line 368, The table cell text
contains "markdown" in lowercase; update that cell to use the proper noun
"Markdown" (e.g., change "Heavy dependencies (markdown parsers, formatters)" to
"Heavy dependencies (Markdown parsers, formatters)") so the term is capitalized
correctly in the row that currently reads "Heavy dependencies (markdown parsers,
formatters) | Server | Dependencies stay off client bundle".
The chunk contamination problem depends on webpack's chunk group iteration order, which is deterministic per build but unpredictable and can change when components are added/removed. Revised the documentation to: - Lead with how to detect the problem (inspect react-client-manifest.json, check Network tab, use webpack-bundle-analyzer) - Explain the mechanism honestly (last-write-wins, iteration-order-dependent) - Present the wrapper as a fix when detected + preventive measure - Remove prescriptive "BAD/GOOD" framing in favor of diagnostic guidance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (8)
docs/migrating/rsc-component-patterns.md (5)
368-368:⚠️ Potential issue | 🟡 MinorCapitalize “Markdown.”
Proper noun in doc tables.
📝 Proposed fix
-| Heavy dependencies (markdown parsers, formatters) | Server | Dependencies stay off client bundle | +| Heavy dependencies (Markdown parsers, formatters) | Server | Dependencies stay off client bundle |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` at line 368, Update the table cell text to capitalize the proper noun "Markdown" — locate the row containing "Heavy dependencies (markdown parsers, formatters) | Server | Dependencies stay off client bundle" and change "markdown" to "Markdown" so it reads "Heavy dependencies (Markdown parsers, formatters) | Server | Dependencies stay off client bundle".
40-58:⚠️ Potential issue | 🟡 MinorAdd a language identifier to the tree diagram fence (MD040).
This fenced block still lacks a language specifier.
🔧 Proposed fix
-``` +```text Before: App ('use client') <-- Everything is client-side🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 40 - 58, Update the fenced code block that contains the component tree diagram so it includes a language identifier (e.g., ```text) on the opening fence; locate the block showing "Before:" / "After:" with App, Layout, Header, Logo, SearchBar, Content, Footer and change the opening ``` to ```text to satisfy the MD040 lint rule.
336-344:⚠️ Potential issue | 🟡 MinorAdd a React 19+ requirement callout for
use(promise).
use()in client components is React 19+; without a version note, React 18 users will hit errors.Does React 19 introduce the `use(promise)` API and is it unavailable in React 18?📝 Proposed fix
+> **Requires React 19+.** The `use(promise)` pattern is not available in React 18. + ```jsx // Comments.jsx -- Client Component 'use client';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 336 - 344, Add a clear callout above the Comments.jsx example stating that use(promise) in client components requires React 19 or later and is not available in React 18; update the text near the Comments component and the use(commentsPromise) example to explicitly mention the React 19+ requirement and that React 18 users will see errors if they try to use this API.
108-122:⚠️ Potential issue | 🟡 MinorRestore the default export in the “After” ProductPage snippet.
The “Before” snippet is a default export; the “After” snippet should match to avoid misleading readers.
📝 Proposed fix
-async function ProductPage({ productId }) { +export default async function ProductPage({ productId }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 108 - 122, The "After" snippet for the ProductPage server component is missing the default export and should match the "Before" example; update the ProductPage definition by adding a default export for the ProductPage component (export default ProductPage) so the component is exported the same way as the "Before" snippet and readers aren't misled—look for the ProductPage function/component declaration in the snippet and append the default export statement.
276-307:⚠️ Potential issue | 🟡 MinorPattern 4 example is missing imports and an export for
Stats.Readers copying this will hit undefined component errors and a failed import of
Stats.📝 Proposed fix
// Dashboard.jsx -- Server Component import { Suspense } from 'react'; +import Stats from './Stats'; +import RevenueChart from './RevenueChart'; +import RecentOrders from './RecentOrders'; +import { StatsSkeleton, ChartSkeleton, TableSkeleton } from './Skeletons';// Stats.jsx -- Async Server Component -async function Stats() { +export default async function Stats() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-component-patterns.md` around lines 276 - 307, The example is missing an export for the Stats component and necessary imports used by both files; export the async Stats function (e.g., export default async function Stats()) and add imports for getStats in Stats.jsx, and import the skeleton components and child components (Stats, RevenueChart, RecentOrders) into Dashboard.jsx (ensure import names/paths match the example filenames), so the components resolve without undefined errors.docs/migrating/rsc-troubleshooting.md (3)
219-230:⚠️ Potential issue | 🔴 CriticalImport
startTransitionfromreact, notnext/navigation.
startTransitionis a React API;next/navigationdoes not export it. Also consider noting this snippet is Next.js-specific.Does `next/navigation` export `startTransition`, or should it be imported from `react`?📝 Proposed fix
-import { useRouter, startTransition } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import { startTransition } from 'react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 219 - 230, The snippet imports startTransition from next/navigation but startTransition is a React API; update the import so startTransition is imported from 'react' (leave useRouter from 'next/navigation'), and ensure the ErrorFallback component and its retry function still call startTransition(() => { router.refresh(); resetErrorBoundary(); }) unchanged; also add a short comment noting this snippet is Next.js-specific if docs require clarification.
87-93:⚠️ Potential issue | 🟡 MinorAdd language identifiers to both diagram fences (MD040).
Both fenced blocks are missing a language specifier.
🔧 Proposed fix
-``` +```text ClientComponent.jsx ('use client') ├── imports utils.js → becomes client code-``` +```text Unit Tests (Vitest/Jest) ├── Client Components -- full support with hooks mockingAlso applies to: 322-338
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 87 - 93, The two fenced diagram blocks (the one starting with "ClientComponent.jsx ('use client')" and the one starting "Unit Tests (Vitest/Jest)") lack language specifiers; update both opening triple-backtick fences to include a language (e.g., ```text) so markdown lint rule MD040 is satisfied, and apply the same fix to the other affected fenced block around lines 322-338 in the same file.
465-470:⚠️ Potential issue | 🟡 MinorAvoid
fetch(process.env.DATABASE_URL)— it’s not an HTTP URL.
DATABASE_URLis typically a DB connection string, so Fetch will fail and the comment is misleading.📝 Proposed fix
async function DBComponent() { - const data = await fetch(process.env.DATABASE_URL); // Works - const secret = process.env.API_SECRET; // Works + const db = new DatabaseClient(process.env.DATABASE_URL); // Works + const secret = process.env.API_SECRET; // Works }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/migrating/rsc-troubleshooting.md` around lines 465 - 470, The example in DBComponent incorrectly uses fetch(process.env.DATABASE_URL) where DATABASE_URL is a database connection string, not an HTTP URL; update the example to either call fetch with a proper HTTP endpoint environment variable (e.g., process.env.API_URL) or show using a database client to connect with the connection string (e.g., reference DBComponent and replace fetch(process.env.DATABASE_URL) with an appropriate DB client call using process.env.DATABASE_URL), and update the inline comment to reflect the correct usage (e.g., "fetch an HTTP API" vs "connect to DB").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@docs/migrating/rsc-component-patterns.md`:
- Line 368: Update the table cell text to capitalize the proper noun "Markdown"
— locate the row containing "Heavy dependencies (markdown parsers, formatters) |
Server | Dependencies stay off client bundle" and change "markdown" to
"Markdown" so it reads "Heavy dependencies (Markdown parsers, formatters) |
Server | Dependencies stay off client bundle".
- Around line 40-58: Update the fenced code block that contains the component
tree diagram so it includes a language identifier (e.g., ```text) on the opening
fence; locate the block showing "Before:" / "After:" with App, Layout, Header,
Logo, SearchBar, Content, Footer and change the opening ``` to ```text to
satisfy the MD040 lint rule.
- Around line 336-344: Add a clear callout above the Comments.jsx example
stating that use(promise) in client components requires React 19 or later and is
not available in React 18; update the text near the Comments component and the
use(commentsPromise) example to explicitly mention the React 19+ requirement and
that React 18 users will see errors if they try to use this API.
- Around line 108-122: The "After" snippet for the ProductPage server component
is missing the default export and should match the "Before" example; update the
ProductPage definition by adding a default export for the ProductPage component
(export default ProductPage) so the component is exported the same way as the
"Before" snippet and readers aren't misled—look for the ProductPage
function/component declaration in the snippet and append the default export
statement.
- Around line 276-307: The example is missing an export for the Stats component
and necessary imports used by both files; export the async Stats function (e.g.,
export default async function Stats()) and add imports for getStats in
Stats.jsx, and import the skeleton components and child components (Stats,
RevenueChart, RecentOrders) into Dashboard.jsx (ensure import names/paths match
the example filenames), so the components resolve without undefined errors.
In `@docs/migrating/rsc-troubleshooting.md`:
- Around line 219-230: The snippet imports startTransition from next/navigation
but startTransition is a React API; update the import so startTransition is
imported from 'react' (leave useRouter from 'next/navigation'), and ensure the
ErrorFallback component and its retry function still call startTransition(() =>
{ router.refresh(); resetErrorBoundary(); }) unchanged; also add a short comment
noting this snippet is Next.js-specific if docs require clarification.
- Around line 87-93: The two fenced diagram blocks (the one starting with
"ClientComponent.jsx ('use client')" and the one starting "Unit Tests
(Vitest/Jest)") lack language specifiers; update both opening triple-backtick
fences to include a language (e.g., ```text) so markdown lint rule MD040 is
satisfied, and apply the same fix to the other affected fenced block around
lines 322-338 in the same file.
- Around line 465-470: The example in DBComponent incorrectly uses
fetch(process.env.DATABASE_URL) where DATABASE_URL is a database connection
string, not an HTTP URL; update the example to either call fetch with a proper
HTTP endpoint environment variable (e.g., process.env.API_URL) or show using a
database client to connect with the connection string (e.g., reference
DBComponent and replace fetch(process.env.DATABASE_URL) with an appropriate DB
client call using process.env.DATABASE_URL), and update the inline comment to
reflect the correct usage (e.g., "fetch an HTTP API" vs "connect to DB").
Review: RSC Migration Guide SeriesThis is a solid, well-structured set of articles that covers real-world RSC migration challenges with accurate patterns. The content is genuinely useful — the donut pattern, chunk contamination walkthrough, Structural concern: Next.js-specific APIs mixed into framework-agnostic contentThe intro (
React on Rails users won't have these APIs available. Each article should include a note like:
Code bugs in examples
Security gap in Server Action exampleThe const content = formData.get('content');
const postId = formData.get('postId');
await db.comments.create({ data: { content, postId } });Server Actions are public HTTP endpoints. The Zod validation pattern is correctly shown later in Minor: React 19 Context syntax used without noting it
Overall: the content is thorough and well-sourced. The main work needed is (1) adding framework-specific disclaimers consistently, (2) fixing the two code bugs, and (3) addressing the Server Action validation gap. |
| value={quantity} | ||
| onChange={(e) => setQuantity(Number(e.target.value))} | ||
| /> | ||
| <button onClick={() => addToCart(productId, quantity)}> |
There was a problem hiding this comment.
addToCart is used here but is never imported or defined anywhere in AddToCartButton.jsx. Readers who copy this example will get a ReferenceError at runtime. This needs either an import statement (e.g., import { addToCart } from '../lib/cart';) or an inline definition.
| <button onClick={() => addToCart(productId, quantity)}> | |
| <button onClick={() => addToCart(productId, quantity)}> |
Suggested fix at the top of the file:
'use client';
import { useState } from 'react';
import { addToCart } from '../lib/cart'; // <-- add this import
export default function AddToCartButton({ productId }) {| export const UserContext = createContext(null); | ||
|
|
||
| export default function UserProvider({ children, userPromise }) { | ||
| return <UserContext value={userPromise}>{children}</UserContext>; |
There was a problem hiding this comment.
The <UserContext value={...}> syntax (without .Provider) was introduced in React 19. React 18 users would need <UserContext.Provider value={userPromise}>. The prerequisites section mentions React 19+, but a brief note here would prevent confusion for anyone who hasn't fully internalized that requirement:
| return <UserContext value={userPromise}>{children}</UserContext>; | |
| export default function UserProvider({ children, userPromise }) { | |
| // React 19+ syntax: <Context value={...}> replaces <Context.Provider value={...}> | |
| return <UserContext value={userPromise}>{children}</UserContext>; | |
| } |
| import { revalidatePath } from 'next/cache'; | ||
|
|
||
| export async function createComment(formData) { | ||
| const content = formData.get('content'); |
There was a problem hiding this comment.
Security gap: Server Actions are public HTTP endpoints reachable by any client. formData.get('content') and formData.get('postId') are unvalidated before hitting the database. postId in particular — if it's not verified to belong to the current user, this is an IDOR vulnerability.
The Zod pattern shown later in rsc-troubleshooting.md is the right approach. Either add a validation step here or at minimum add a comment redirecting readers to that section:
| const content = formData.get('content'); | |
| export async function createComment(formData) { | |
| // Server Actions are public endpoints — always validate input. | |
| // See the TypeScript section in rsc-troubleshooting.md for a Zod example. | |
| const content = formData.get('content'); | |
| const postId = formData.get('postId'); |
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; |
There was a problem hiding this comment.
startTransition is a core React API and should be imported from react, not next/navigation. The current import will break in any non-Next.js RSC setup (including React on Rails).
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; |
…nation - Add "'use client' Marks a Boundary, Not a Component Type" subsection to Mental Model Shift in component patterns (simple, no webpack details) - Move chunk contamination from Common Mistakes to troubleshooting guide (next to existing Import Chain Contamination section), with full webpack diagnostic details including the redundant-directive risk - Add cross-references from: entry point doc, third-party libs thin wrapper intro, and troubleshooting directive mistakes section - Replace Mistake 3 in component patterns with brief summary + link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review: RSC Migration Guide SeriesThis is a well-structured, comprehensive series covering the right topics. The writing is clear, the patterns are accurate, and the cross-references between articles work correctly. A few issues worth addressing before merge: Critical Bug
// Incorrect
import { useRouter, startTransition } from 'next/navigation';
// Correct
import { startTransition } from 'react';
import { useRouter } from 'next/navigation';
Significant: Next.js-Specificity Not Framed for React on Rails UsersMany examples use Next.js-specific APIs ( The intro (
Security: Server Action Examples Show No Input ValidationThe Server Action examples in Minor: Incomplete Code Example
Minor: styled-components Entry Slightly ConfusingThe CSS-in-JS table says styled-components is in "Maintenance mode (March 2025). v6.3.0+ added RSC support" — the juxtaposition reads as contradictory. Consider splitting these into two cells or rephrasing: "v6.3.0+ added RSC support before entering maintenance mode in March 2025." Overall this is high-quality documentation. The chunk contamination section in particular is excellent — that's a subtle, real-world issue that most guides skip. |
| import { useRouter, startTransition } from 'next/navigation'; | ||
| import { ErrorBoundary } from 'react-error-boundary'; |
There was a problem hiding this comment.
startTransition is not exported from next/navigation — it's a React core API. This import will fail at runtime.
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { ErrorBoundary } from 'react-error-boundary'; | |
| import { startTransition } from 'react'; | |
| import { useRouter } from 'next/navigation'; | |
| import { ErrorBoundary } from 'react-error-boundary'; |
| <button onClick={() => addToCart(productId, quantity)}> | ||
| Add to Cart | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
addToCart is called here without being imported or defined anywhere in this file. Readers copying this example will get a ReferenceError. Consider either adding an import comment or converting the handler to a Server Action:
| </div> | |
| <button onClick={() => addToCart(productId, quantity)}> |
A simple fix would be adding at the top of the file:
import { addToCart } from './cart-actions'; // or wherever your cart logic livesOr make it a no-op stub with a comment indicating it should be replaced with a real implementation.
|
|
||
| ```jsx | ||
| // actions.js | ||
| 'use server'; | ||
|
|
||
| import { revalidatePath } from 'next/cache'; | ||
|
|
||
| export async function createComment(formData) { |
There was a problem hiding this comment.
Server Actions are public HTTP endpoints — any client can call them with arbitrary formData. This example writes unvalidated user input directly to the database, which is unsafe and sets a bad precedent for readers.
Consider adding a brief inline note pointing to the Zod validation example in rsc-troubleshooting.md, or show minimal validation here:
export async function createComment(formData) {
const content = formData.get('content');
const postId = formData.get('postId');
// Always validate Server Action inputs — they are public endpoints.
// See the TypeScript section in rsc-troubleshooting.md for a full Zod example.
if (typeof content !== 'string' || !content.trim() || typeof postId !== 'string') {
return { error: 'Invalid input' };
}
await db.comments.create({ data: { content, postId } });
revalidatePath(`/posts/${postId}`);
}|
|
||
| | Library | RSC Status | Notes | | ||
| |---------|-----------|-------| | ||
| | **styled-components** | Maintenance mode (March 2025). v6.3.0+ added RSC support via React's `<style>` tag hoisting. | The maintainer stated: "For new projects, I would not recommend adopting styled-components." React Context dependency is the root incompatibility. | |
There was a problem hiding this comment.
The juxtaposition of "Maintenance mode" and "v6.3.0+ added RSC support" in the same cell reads as contradictory. Consider clarifying the timeline:
| styled-components | Partial (v6.3.0+) | Added RSC support via React's <style> tag hoisting before entering maintenance mode in March 2025. The maintainer recommends against adopting it for new projects. React Context dependency is the root incompatibility. |
Rails users shouldn't bypass Rails layers to access the DB directly from Server Components. Async props let Rails send data incrementally through its normal controller/view layers, with Suspense-based streaming. Added dedicated section with ERB helper, component pattern, TypeScript typing, and cross-reference from Direct Database Access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: RSC Migration Guide SeriesThis is a well-structured and comprehensive documentation addition. The multi-article format, progressive migration strategy, and code examples are genuinely helpful. A few issues to address: Bug: Wrong import for
|
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; |
There was a problem hiding this comment.
startTransition is a React API, not part of next/navigation. This import will fail at runtime.
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; |
| const posts = await getPosts(user.id); // 250ms (waits for stats) | ||
| } | ||
|
|
||
| // GOOD: Parallel fetching (300ms total) |
There was a problem hiding this comment.
The "GOOD" example silently changes the inputs: the "BAD" version uses user.id for downstream calls (because user is fetched first), while this version uses userId as a pre-existing prop. These are different data dependency scenarios.
The example is misleading because it implies the only change is wrapping with Promise.all. Readers should understand that this refactoring is only possible when userId is available upfront — which may require restructuring callers to pass the ID rather than the hydrated object. Consider adding a comment clarifying this prerequisite.
|
|
||
| - **62% reduction** in client-side bundle size ([Frigade case study](https://frigade.com/blog/bundle-size-reduction-with-rsc-and-frigade)) | ||
| - **63% improvement** in Google Speed Index | ||
| - Total blocking time reduced **from 110ms to 1ms** |
There was a problem hiding this comment.
The Frigade citation is present for the bundle size reduction, but the next two metrics ("63% improvement in Google Speed Index" and "Total blocking time reduced from 110ms to 1ms") are uncited. Please add a source for these — if they're also from the Frigade case study, link there; otherwise attribute to the appropriate study.
|
|
||
| ### The Fundamental Challenge | ||
|
|
||
| Async Server Components are a new paradigm that existing testing tools were not designed for. **Vitest does not support async Server Components** as of early 2026. |
There was a problem hiding this comment.
This claim should be versioned and linked. @testing-library/react has been adding RSC support and the React team has published guidance on testing async Server Components. Saying "Vitest does not support" without a tracking issue or version reference will become outdated quickly and may already be inaccurate.
React on Rails users don't access the DB directly from Server Components or fetch from API routes — they receive data from Rails as props. Removed Direct Database Access section, replaced fetch() "After" example with props-from-Rails pattern, replaced db/fetch calls in React.cache() and Server Actions examples with generic functions, and added Rails controller context for mutations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review: RSC Migration Guide SeriesThis is a well-structured and comprehensive set of guides — the content is genuinely useful and the organization (five focused articles with cross-references) is solid. Good call on moving chunk contamination to the troubleshooting guide and keeping the component patterns article clean. A few issues worth addressing before merge: Bug:
|
| ```jsx | ||
| 'use client'; | ||
|
|
||
| import { useRouter, startTransition } from 'next/navigation'; |
There was a problem hiding this comment.
startTransition is not exported from next/navigation — it comes from React. This import will fail at runtime.
| import { useRouter, startTransition } from 'next/navigation'; | |
| import { useRouter } from 'next/navigation'; | |
| import { startTransition } from 'react'; |
|
|
||
| > **"A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework."** | ||
|
|
||
| **Why it happens:** React tracks promises passed to `use()` by their call-position index across re-renders. On each render, it checks whether the promise at index N is the same object reference as the last render. When you create a promise inside a Client Component, every render produces a new promise instance -- React sees a different reference and throws. |
There was a problem hiding this comment.
The "call-position index" explanation is the rules-of-hooks tracking mechanism and doesn't accurately describe why use() throws here. The real reason: every render creates a new Promise object with a different object reference. use() internally suspends on the promise's identity — if the reference changes between renders, React treats it as a new, uncached promise and throws rather than risk an infinite suspension loop.
Suggested replacement for this paragraph:
Why it happens: Every render creates a new Promise object with a different reference.
use()internally tracks promise state by reference — when the reference changes between renders, React sees a brand-new uncached promise and throws to avoid infinite suspension loops. The only safe sources of a stable promise reference are: a Server Component prop (created once per request) or a Suspense-compatible library that manages promise identity internally (e.g. TanStack Query's cache key).
| const content = formData.get('content'); | ||
| const postId = formData.get('postId'); | ||
|
|
||
| await fetch('/api/comments', { |
There was a problem hiding this comment.
A Server Action making an HTTP request to an API route on the same server is an anti-pattern — it adds a full network round-trip for no benefit. Server Actions run in the server environment and can call the business/service layer directly.
For a React on Rails project, this mutation should go through a Rails controller via a standard form submission or API call from the client, not through a Server Action that re-fetches from the server. If a Server Action is appropriate here, replace the fetch with a direct service call:
export async function createComment(formData) {
const content = formData.get('content');
const postId = formData.get('postId');
// Call service layer directly — no HTTP hop
await CommentService.create({ content, postId });
}|
|
||
| ```jsx | ||
| // layout.jsx -- Server Component | ||
| import { cookies } from 'next/headers'; |
There was a problem hiding this comment.
next/headers is a Next.js-only API. React on Rails does not expose this module. This example will not work as written in a React on Rails project.
Consider either labeling this section as "Next.js example — adapt for your framework" or replacing the cookie reading with a framework-agnostic approach (e.g., reading auth state from props passed from the Rails controller).
| export async function submitForm(formData) { | ||
| const name = formData.get('name'); | ||
| await db.users.create({ data: { name } }); | ||
| revalidatePath('/users'); |
There was a problem hiding this comment.
revalidatePath is a Next.js-only API (next/cache). It is not available in React on Rails. This will cause a runtime error in a React on Rails project.
Consider removing it from this example, or annotating the example as Next.js-specific. In React on Rails, cache invalidation is handled through Rails' cache mechanisms.
| // next.config.js | ||
| module.exports = { | ||
| experimental: { | ||
| optimizePackageImports: ['lucide-react', '@mui/material', 'my-lib'], |
There was a problem hiding this comment.
optimizePackageImports is a Next.js-only configuration option under experimental. It is not available in React on Rails or other RSC frameworks. Consider labeling this section clearly as Next.js-specific, or noting the framework-agnostic alternative: use direct (non-barrel) imports in your own code, and for third-party packages, check if they support exports field path mapping in their package.json.
Summary
Articles created:
Closes #2459
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit