React 19 Mastery Skill - A Claude Code skill recipe for expert React 19 guidance
These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.
Recipe
The complete SKILL.md content you can copy into .claude/skills/react-19-mastery/SKILL.md:
---
name: react-19-mastery
description: "Expert guidance on React 19 features, compiler, actions, and new hooks. Use when asked to: React 19 help, new hooks, server components, React compiler, useActionState, useOptimistic, use() hook, migrate from React 18."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# React 19 Mastery
You are a React 19 expert. When this skill is invoked, provide authoritative guidance on React 19 features, migration, and best practices.
## Core React 19 Features Reference
### New Hooks
1. **useActionState(action, initialState, permalink?)** - Manages form/action state with pending tracking
- Returns [state, formAction, isPending]
- Replaces useFormState (renamed in React 19 stable)
- Use for: any form submission, server action integration, progressive enhancement
- The action receives previousState as its first argument
2. **useOptimistic(state, updateFn)** - Instant UI feedback during async operations
- Returns [optimisticState, addOptimistic]
- Automatically reverts when the parent rerender completes
- Use for: likes, bookmarks, toggles, any mutation where you can predict the result
3. **useFormStatus()** - Read parent form submission status from a child component
- Returns { pending, data, method, action }
- MUST be called from a component rendered inside a <form>
- Use for: submit button disabled states, loading spinners inside forms
4. **use(resource)** - Unwrap promises and context conditionally
- Works with Promises (Suspense-aware) and Context
- CAN be called inside conditionals and loops (unlike other hooks)
- Use for: reading async data in render, conditional context reads
### React Compiler
- Automatically memoizes components, hooks, and expressions
- Eliminates most manual useMemo, useCallback, and React.memo usage
- Rules of React must be followed strictly (no mutations during render, stable hook call order)
- Install: babel-plugin-react-compiler or the Vite/Next.js plugin
- Validate with eslint-plugin-react-compiler before enabling
### Server Components (RSC)
- Default in App Router (no "use client" directive = Server Component)
- Can directly access databases, file systems, and server-only APIs
- Cannot use hooks, event handlers, browser APIs, or state
- Props passed to Client Components must be serializable
### Server Actions
- Async functions marked with "use server" at the top of the function body or file
- Can be passed to forms as action={serverAction}
- Can be called from Client Components like regular async functions
- Always validate inputs server-side even if client validation exists
### ref as prop
- Function components now accept ref as a regular prop
- forwardRef is no longer needed (still works but deprecated)
- Pattern: function Input({ ref, ...props }) { return <input ref={ref} {...props} /> }
### Document Metadata Hoisting
- <title>, <meta>, and <link> tags in components auto-hoist to <head>
- Works in both Server and Client Components
- Eliminates need for next/head or react-helmet in many cases
## Decision Rules
When a user asks about React 19, follow these rules:
### When to recommend useActionState
- User is building a form that submits data
- User needs to track pending state during submission
- User wants progressive enhancement (works without JS)
- User is migrating from useFormState
### When to recommend useOptimistic
- User wants instant feedback on a mutation
- The optimistic result is predictable from the input
- User is building like/bookmark/toggle features
- ALWAYS pair with error handling to revert on failure
### When to recommend use()
- User needs to read a promise in a component (wrap parent in Suspense)
- User needs conditional context access
- Do NOT recommend for initial data fetching in Next.js (use async Server Components instead)
### When to recommend React Compiler
- User is on React 19 and has a clean codebase following Rules of React
- User has performance issues from missing memoization
- ALWAYS recommend running eslint-plugin-react-compiler first
- Do NOT recommend if codebase has mutation patterns or side effects in render
## Migration Patterns from React 18
### useFormState to useActionState
```tsx
// React 18 (react-dom)
import \{ useFormState \} from "react-dom";
const [state, formAction] = useFormState(action, initial);
// React 19 (react)
import \{ useActionState \} from "react";
const [state, formAction, isPending] = useActionState(action, initial);
// Note: isPending is now built-in, no need for useFormStatus just for pendingforwardRef to ref prop
// React 18
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref=\{ref\} \{...props\} />
));
// React 19
function Input(\{ ref, ...props \}: Props & \{ ref?: React.Ref<HTMLInputElement> \}) \{
return <input ref=\{ref\} \{...props\} />;
\}Manual memo to React Compiler
// React 18 - manual memoization
const MemoComponent = React.memo((\{ data \}: Props) => \{
const processed = useMemo(() => expensive(data), [data]);
const handler = useCallback(() => doThing(data), [data]);
return <Child data=\{processed\} onClick=\{handler\} />;
\});
// React 19 with Compiler - just write plain code
function Component(\{ data \}: Props) \{
const processed = expensive(data);
const handler = () => doThing(data);
return <Child data=\{processed\} onClick=\{handler\} />;
\}Common Mistakes
- Using useFormStatus outside a form - It reads status from the nearest parent
formelement, not from useActionState - Passing non-serializable props to Client Components - Functions, classes, and Dates cannot cross the server/client boundary
- Using use() without Suspense - Promises passed to use() require a Suspense boundary above
- Mutating state in render with Compiler enabled - The compiler assumes Rules of React; mutations break memoization
- Calling Server Actions during render - They are async and must be called from event handlers or form actions
- Forgetting "use server" directive - Must be at the top of the function body or file, not just an export
## Working Example
### Example 1: User asks "Help me build a form with React 19"
**User prompt:** "I need to build a comment form that shows optimistic updates"
**Skill-guided response would include:**
```tsx
// actions.ts
"use server";
import { z } from "zod";
const CommentSchema = z.object({
text: z.string().min(1).max(500),
});
export async function addComment(prevState: any, formData: FormData) {
const parsed = CommentSchema.safeParse({
text: formData.get("text"),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
await db.comment.create({ data: { text: parsed.data.text } });
return { error: null };
}
// CommentForm.tsx
"use client";
import { useActionState, useOptimistic } from "react";
import { addComment } from "./actions";
import { SubmitButton } from "./SubmitButton";
type Comment = { id: string; text: string };
export function CommentForm({ comments }: { comments: Comment[] }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newText: string) => [
...state,
{ id: crypto.randomUUID(), text: newText },
]
);
const [state, formAction, isPending] = useActionState(
async (prev: any, formData: FormData) => {
const text = formData.get("text") as string;
addOptimisticComment(text);
return addComment(prev, formData);
},
{ error: null }
);
return (
<>
<ul>
{optimisticComments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
<form action={formAction}>
<input name="text" required />
{state.error && <p className="text-red-500">{state.error.text}</p>}
<SubmitButton />
</form>
</>
);
}// SubmitButton.tsx
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Posting..." : "Post Comment"}
</button>
);
}Example 2: User asks "Should I use React Compiler?"
Skill-guided response would include:
- Run
npx eslint-plugin-react-compileron the codebase first - Check for mutation patterns in render functions
- Recommend enabling in one route/page first, not the whole app
- Show before/after of removing manual memoization
Deep Dive
How the Skill Works
This skill turns Claude into a React 19 specialist by providing:
- Feature reference - Complete API signatures and usage rules for every new React 19 feature
- Decision rules - When-to-use logic that prevents recommending the wrong feature for a given scenario
- Migration patterns - Side-by-side React 18 vs 19 code for the most common upgrades
- Anti-pattern detection - A list of common mistakes so Claude can proactively warn about them
Customization
You can extend this skill by:
- Adding your project-specific conventions (e.g., "always use our
useServerActionwrapper") - Restricting recommendations (e.g., "do not recommend React Compiler until we complete the audit")
- Adding migration patterns for your specific React 18 patterns (e.g., custom form hooks)
How to Install
Create the file .claude/skills/react-19-mastery/SKILL.md in your project root and paste the complete content from the Recipe section above.
mkdir -p .claude/skills/react-19-mastery
# Then paste the Recipe content into .claude/skills/react-19-mastery/SKILL.mdGotchas
- useActionState was renamed from useFormState between the RC and stable release. Many blog posts and tutorials still use the old name. The skill accounts for this.
- React Compiler is opt-in and ships as a separate package. It is not automatically enabled by upgrading to React 19.
- use() is not useEffect - it does not run side effects. It unwraps resources during render.
- Server Components cannot be imported into Client Components - the boundary is one-way. Client Components can receive Server Components as children via props.
Alternatives
| Approach | When to Use |
|---|---|
| React 18 + manual optimization | Legacy projects not ready to migrate |
| Preact Signals | Smaller bundle, different reactivity model |
| Solid.js | Fine-grained reactivity without virtual DOM |
| Svelte 5 | Compiler-first approach with runes |
FAQs
What are the four new hooks introduced in React 19?
useActionState(action, initialState)-- manages form/action state with pending trackinguseOptimistic(state, updateFn)-- instant UI feedback during async operationsuseFormStatus()-- reads parent form submission status from a child componentuse(resource)-- unwraps promises and context, can be called inside conditionals
How does useActionState differ from the old useFormState?
// React 18 (from react-dom)
const [state, formAction] = useFormState(action, initial);
// React 19 (from react)
const [state, formAction, isPending] = useActionState(action, initial);- Renamed from
useFormStatetouseActionState - Moved from
react-domtoreact - Now returns
isPendingas a third value (no need for separateuseFormStatusjust for pending)
What does the React Compiler do and when should you enable it?
- Automatically memoizes components, hooks, and expressions
- Eliminates most manual
useMemo,useCallback, andReact.memousage - Only enable after running
eslint-plugin-react-compilerto check for violations - Do not enable if the codebase has mutation patterns or side effects in render
How does ref as a prop work in React 19 and what does it replace?
// React 19 -- ref is a regular prop
function Input({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}forwardRefis no longer needed (still works but is deprecated)- Function components accept
refdirectly as a prop
Gotcha: What happens if you use useFormStatus outside of a form?
- It reads the status of the nearest parent
<form>element - If there is no parent form, it will not detect any submission state
- Always render the component using
useFormStatusinside a<form>, not in the same component that creates the form
When should you use useOptimistic and what happens on failure?
- Use when you can predict the result of a mutation (likes, bookmarks, toggles)
- It provides instant UI feedback before the async operation completes
- If the operation fails, the optimistic state automatically reverts when the parent rerenders
- Always pair with error handling to revert on failure
What are the rules for Server Components in the App Router?
- Default in App Router (no
"use client"directive means Server Component) - Can directly access databases, file systems, and server-only APIs
- Cannot use hooks, event handlers, browser APIs, or state
- Props passed to Client Components must be serializable
Gotcha: Can you pass non-serializable props from a Server Component to a Client Component?
- No. Functions, classes, and Date objects cannot cross the server/client boundary
- Only serializable data (strings, numbers, plain objects, arrays) can be passed as props
- Convert non-serializable values to serializable formats before passing them
How do you type useActionState with TypeScript for a form action?
type FormState = { error: string | null; success: boolean };
const [state, formAction, isPending] = useActionState(
async (prev: FormState, formData: FormData): Promise<FormState> => {
// ...
return { error: null, success: true };
},
{ error: null, success: false }
);- The action receives
previousStateas its first argument andFormDataas second - The return type must match the initial state type
How does the use() hook differ from useEffect?
use()unwraps resources (Promises, Context) during render, not as a side effect- It requires a
<Suspense>boundary above when used with Promises - Unlike other hooks, it CAN be called inside conditionals and loops
- Do not use for initial data fetching in Next.js -- use async Server Components instead
What is the migration path from React.memo and useMemo to the React Compiler?
// React 18 -- manual memoization
const MemoComponent = React.memo(({ data }: Props) => {
const processed = useMemo(() => expensive(data), [data]);
return <Child data={processed} />;
});
// React 19 with Compiler -- plain code
function Component({ data }: Props) {
const processed = expensive(data);
return <Child data={processed} />;
}- The Compiler handles memoization automatically
- Remove
React.memo,useMemo, anduseCallbackwrappers
What does document metadata hoisting mean in React 19?
<title>,<meta>, and<link>tags in components auto-hoist to<head>- Works in both Server and Client Components
- Eliminates the need for
next/headorreact-helmetin many cases