React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

skillsreact-19hooksserver-componentscompileractions

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 pending

forwardRef 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

  1. Using useFormStatus outside a form - It reads status from the nearest parent form element, not from useActionState
  2. Passing non-serializable props to Client Components - Functions, classes, and Dates cannot cross the server/client boundary
  3. Using use() without Suspense - Promises passed to use() require a Suspense boundary above
  4. Mutating state in render with Compiler enabled - The compiler assumes Rules of React; mutations break memoization
  5. Calling Server Actions during render - They are async and must be called from event handlers or form actions
  6. 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-compiler on 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:

  1. Feature reference - Complete API signatures and usage rules for every new React 19 feature
  2. Decision rules - When-to-use logic that prevents recommending the wrong feature for a given scenario
  3. Migration patterns - Side-by-side React 18 vs 19 code for the most common upgrades
  4. 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 useServerAction wrapper")
  • 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.md

Gotchas

  • 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

ApproachWhen to Use
React 18 + manual optimizationLegacy projects not ready to migrate
Preact SignalsSmaller bundle, different reactivity model
Solid.jsFine-grained reactivity without virtual DOM
Svelte 5Compiler-first approach with runes

FAQs

What are the four new hooks introduced in React 19?
  • useActionState(action, initialState) -- manages form/action state with pending tracking
  • useOptimistic(state, updateFn) -- instant UI feedback during async operations
  • useFormStatus() -- reads parent form submission status from a child component
  • use(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 useFormState to useActionState
  • Moved from react-dom to react
  • Now returns isPending as a third value (no need for separate useFormStatus just 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, and React.memo usage
  • Only enable after running eslint-plugin-react-compiler to 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} />;
}
  • forwardRef is no longer needed (still works but is deprecated)
  • Function components accept ref directly 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 useFormStatus inside 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 previousState as its first argument and FormData as 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, and useCallback wrappers
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/head or react-helmet in many cases