modern-react

SKILL.md

Modern React (19.x) Complete Guide

This skill covers React 19.0 through 19.2. It is structured so an LLM can quickly identify what pattern to use, what to avoid, and how to migrate legacy code. Every section follows: what it isDO thisDON'T do thatmigration path.

Table of Contents

  1. Critical DO / DON'T Quick Reference
  2. Migration Mapping Table
  3. New Hooks Deep Dive
  4. Actions and Forms
  5. React 19.2 Features
  6. Server Components
  7. Server Component DO / DON'T
  8. Complete Migration Examples
  9. Common Anti-Patterns to Fix
  10. Codemod Commands
  11. Breaking Changes
  12. Migration Checklist
  13. Quick Reference Card

Critical DO / DON'T Quick Reference

These tables are the fastest way to understand what modern React expects. When reviewing or writing React code, check here first.

Hooks — DO / DON'T

DO (Modern React 19) DON'T (Legacy/Anti-pattern) Why
useActionState(action, initialState) for form state Manual useState + setIsPending + setError for forms useActionState handles pending, error, and reset automatically
useOptimistic(state, updateFn) for instant UI feedback Separate optimisticState + manual try/catch rollback React auto-rolls back on error, less code, fewer bugs
useFormStatus() in child components for form pending state Prop-drilling isPending from parent form to children useFormStatus reads parent form state directly, zero props
use(MyContext) — can be conditional useContext(MyContext) — must be top-level use() works inside if/loops/early returns
use(promise) with <Suspense> for data reading useEffect + useState for data fetching Suspense-based reading is declarative and avoids race conditions
useEffectEvent(() => { ... }) for non-reactive Effect logic (19.2) Adding values to Effect deps just because they're referenced useEffectEvent breaks false dependencies without hiding bugs
Import useActionState from 'react' Import useFormState from 'react-dom' useFormState is deprecated and removed
Import useFormStatus from 'react-dom' N/A — this is new, no old equivalent Only hook that stays in react-dom

Refs — DO / DON'T

DO (Modern React 19) DON'T (Legacy/Anti-pattern) Why
function Input({ ref, ...props }) — ref as regular prop forwardRef((props, ref) => ...) wrapper forwardRef is unnecessary boilerplate in React 19
const myRef = useRef(null) + <input ref={myRef} /> <input ref="myInput" /> string refs String refs are removed in React 19 — they will throw
Access ref via myRef.current Access ref via this.refs.myInput this.refs pattern is removed

Context — DO / DON'T

DO (Modern React 19) DON'T (Legacy/Anti-pattern) Why
<ThemeContext value={theme}> directly <ThemeContext.Provider value={theme}> .Provider is no longer needed — Context itself is the provider
const value = use(MyContext) const value = useContext(MyContext) use() can be called conditionally; useContext cannot

Rendering & Mounting — DO / DON'T

DO (Modern React 19) DON'T (Legacy/Removed) Why
createRoot(el).render(<App />) ReactDOM.render(<App />, el) ReactDOM.render is removed in React 19
hydrateRoot(el, <App />) ReactDOM.hydrate(<App />, el) ReactDOM.hydrate is removed in React 19
Native <title>, <meta> in components react-helmet or manual document.title React 19 hoists metadata natively — no library needed
<Activity mode="hidden"> to preserve hidden state (19.2) {condition && <Component />} to show/hide Conditional rendering destroys state; Activity preserves it

Forms — DO / DON'T

DO (Modern React 19) DON'T (Legacy/Anti-pattern) Why
<form action={formAction}> with useActionState <form onSubmit={handleSubmit}> with e.preventDefault() Actions handle pending, error, and reset automatically
Let React auto-reset form after successful action Manually clearing each useState('') after submit Forms with action functions auto-reset on success
Use formData.get('name') from FormData Controlled inputs with value={name} onChange={...} for every field Uncontrolled + FormData is simpler for submission-focused forms
Combine useActionState + useOptimistic for best UX Build separate optimistic state management by hand The two hooks are designed to work together

Server Components — DO / DON'T

DO (Modern React 19) DON'T (Anti-pattern) Why
Keep components as Server Components by default Add 'use client' to every file Server Components have zero bundle cost
Add 'use client' only when you need hooks/events/browser APIs Use 'use client' for data fetching components Fetch data on the server; send only the result
Use 'use server' for Server Actions (data mutations) Build separate API routes for every form submission Server Actions are simpler and type-safe
Pass serializable props from Server → Client components Try to import Server Components inside Client Components Client Components can only receive serializable data
Access databases/APIs directly in Server Components Create API endpoints just to fetch data for rendering Server Components run on the server — direct access is the point

Migration Mapping Table

This table maps every deprecated/removed API to its modern replacement with the exact import change. Use this as a lookup when you encounter legacy code.

Legacy Pattern Modern Replacement Import Change Codemod
ReactDOM.render(<App />, el) createRoot(el).render(<App />) 'react-dom''react-dom/client' react/replace-reactdom-render
ReactDOM.hydrate(<App />, el) hydrateRoot(el, <App />) 'react-dom''react-dom/client' react/replace-reactdom-render
useFormState(action, init) useActionState(action, init) 'react-dom''react' react/replace-use-form-state
useContext(MyContext) use(MyContext) same 'react' react/use-context-hook
forwardRef((props, ref) => ...) function Comp({ ref, ...props }) Remove forwardRef import react/remove-forward-ref
<Ctx.Provider value={v}> <Ctx value={v}> No import change react/remove-context-provider
ref="myInput" / this.refs.x useRef(null) + ref={myRef} No import change react/replace-string-ref
import { act } from 'react-dom/test-utils' import { act } from 'react' 'react-dom/test-utils''react' react/replace-act-import
React.createElement('div', props) <div {...props} /> (JSX) No import change react/create-element-to-jsx
import React from 'react' import { useState } from 'react' Named imports react/update-react-imports
React.PropTypes import PropTypes from 'prop-types' or TypeScript Separate package react/React-PropTypes-to-prop-types
React.createFactory('div') JSX or createElement No import change react/create-element-to-jsx
<Helmet><title>X</title></Helmet> <title>X</title> directly in component Remove react-helmet Manual

New Hooks Deep Dive

useActionState

What it is: A single hook that replaces manual useState + setIsPending + setError boilerplate for form submissions. It tracks the action's return value as state, wraps the action for use with <form action={...}>, and provides an isPending boolean.

API:

import { useActionState } from 'react';

const [state, formAction, isPending] = useActionState(action, initialState);
// action:       async (previousState, formData) => newState
// initialState: initial value of state (before first submission)
// state:        current state (last return value of action, or initialState)
// formAction:   wrapped action to pass to <form action={formAction}>
// isPending:    boolean — true while action is running

Example — before and after:

// ---- DON'T: React 18 manual pattern ----
function UpdateName({ name, setName }) {
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) setError(error);
  };

  return (
    <button onClick={handleSubmit} disabled={isPending}>Update</button>
  );
}

// ---- DO: React 19 useActionState ----
import { useActionState } from 'react';

function ChangeName() {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get('name'));
      if (error) return error;
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

useOptimistic

What it is: Shows an immediate, temporary state update while an async operation runs. If the operation fails, React automatically rolls back to the real state — no manual try/catch needed.

API:

import { useOptimistic } from 'react';

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
// state:           the actual/confirmed state
// updateFn:        (currentState, optimisticValue) => newOptimisticState
// optimisticState: state with optimistic updates applied (reverts on error)
// addOptimistic:   function to trigger an optimistic update

Example — before and after:

// ---- DON'T: Manual optimistic + rollback ----
function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [optimisticLikes, setOptimisticLikes] = useState(initialLikes);

  const handleLike = async () => {
    setOptimisticLikes(prev => prev + 1);
    try {
      const newLikes = await likePost();
      setLikes(newLikes);
    } catch (error) {
      setOptimisticLikes(likes); // Manual rollback — easy to forget
    }
  };

  return <button>{optimisticLikes} Likes</button>;
}

// ---- DO: useOptimistic with automatic rollback ----
import { useOptimistic, useState } from 'react';

function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (currentLikes, addedLikes) => currentLikes + addedLikes
  );

  const handleLike = async () => {
    addOptimisticLike(1);              // Instant UI update
    const newLikes = await likePost();
    setLikes(newLikes);                // Confirm real state
    // React auto-rolls back on error — no catch needed
  };

  return <button>{optimisticLikes} Likes</button>;
}

useFormStatus

What it is: Lets any child component of a <form> read the form's submission status without prop drilling. Must be imported from react-dom.

API:

import { useFormStatus } from 'react-dom';

const { pending, data, method, action } = useFormStatus();
// pending: boolean — is the parent form currently submitting
// data:    FormData — the submitted form data
// method:  'get' | 'post'
// action:  function — the form's action function

Example — before and after:

// ---- DON'T: Prop drilling isPending ----
function SubmitButton({ isPending }) {
  return (
    <button type="submit" disabled={isPending}>
      {isPending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function Form() {
  const [isPending, setIsPending] = useState(false);
  return <form><SubmitButton isPending={isPending} /></form>;
}

// ---- DO: useFormStatus reads parent form directly ----
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function Form() {
  return (
    <form action={handleSubmit}>
      <input name="email" />
      <SubmitButton /> {/* Zero props needed */}
    </form>
  );
}

Important: The component calling useFormStatus must be rendered inside a <form>. It reads the nearest parent form's status. Calling it in the same component that renders the <form> will not work — it must be a child.


use() Hook

What it is: Reads the value of a Promise or Context during rendering. The key difference from useContext: use() can be called conditionally (inside if statements, loops, after early returns). For Promises, it integrates with <Suspense>.

Reading Promises:

import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // Suspends until resolved

  return (
    <ul>
      {comments.map(comment => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

function Page({ commentsPromise }) {
  return (
    <Suspense fallback={<div>Loading comments...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

Reading Context conditionally:

// ---- DON'T: useContext must be top-level, always runs ----
function Component({ condition }) {
  const value = useContext(MyContext); // Runs even if not needed
  if (!condition) return null;
  return <div>{value}</div>;
}

// ---- DO: use() can be conditional ----
function Component({ condition }) {
  if (!condition) return null;
  const value = use(MyContext); // Only runs when needed
  return <div>{value}</div>;
}

Actions and Forms

What Are Actions?

Actions are React 19's pattern for data mutations. Any function that uses async transitions is called an "Action". React automatically manages:

  • Pending statesisPending is tracked for you
  • Optimistic updates — via useOptimistic
  • Error handling — action return value becomes the state
  • Form reset — forms with action={fn} auto-reset on success

Form Action DO / DON'T

DO DON'T Why
<form action={submitAction}> <form onSubmit={(e) => { e.preventDefault(); ... }}> Actions handle pending/error/reset automatically
Return error state from action: return { error: 'msg' } throw new Error() inside action Thrown errors go to error boundaries; returned errors stay in component state
Use formData.get('field') to read form values Controlled value={x} onChange={...} for submit-only forms Uncontrolled inputs + FormData = less state, less re-rendering
Combine useActionState + useOptimistic Build separate optimistic update logic They are designed as companions

Complete Action + Optimistic Updates Pattern

This is the recommended pattern for forms that need instant UI feedback:

import { useOptimistic, useActionState, useState } from 'react';

function TodoList({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  );

  const [error, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const title = formData.get('title');
      addOptimisticTodo({ id: Date.now(), title });
      const newTodo = await addTodo(title);
      setTodos(prev => [...prev, newTodo]);
      return null;
    },
    null
  );

  return (
    <div>
      <form action={submitAction}>
        <input name="title" />
        <button type="submit" disabled={isPending}>Add</button>
      </form>
      {error && <p className="error">{error}</p>}
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

React 19.2 Features

Activity Component

What it is: Wraps UI that can be shown or hidden while preserving its full state (including form inputs, scroll position, internal component state). Unlike {condition && <Component />} which destroys and remounts, Activity keeps everything alive.

Modes:

  • 'visible' — shows children, mounts effects, normal updates
  • 'hidden' — hides children (CSS visibility), unmounts effects, defers updates until idle
import { Activity } from 'react';

// ---- DON'T: Conditional rendering destroys state ----
{showPage && <Page />}   // State lost when hidden

// ---- DO: Activity preserves state ----
<Activity mode={showPage ? 'visible' : 'hidden'}>
  <Page />                {/* State preserved when hidden */}
</Activity>

Use cases:

// Pre-render tabs before user clicks
<Activity mode="hidden">
  <TabContent tab="settings" />
</Activity>

// Preserve form state when navigating away
<Activity mode={isOnFormPage ? 'visible' : 'hidden'}>
  <ComplexForm />
</Activity>

// Background loading for faster navigation
<Activity mode="hidden">
  <NextPageDataLoader />
</Activity>

useEffectEvent

What it is: Extracts non-reactive logic from Effects. Creates event handlers that always see the latest props/state without being listed as Effect dependencies. This solves the problem of Effects re-running when values change that shouldn't trigger re-subscription.

// ---- DON'T: Effect re-runs on theme change (unnecessary) ----
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme); // theme forces re-run
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]); // Re-subscribes when theme changes!
}

// ---- DO: useEffectEvent breaks the false dependency ----
import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme); // Always sees latest theme
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => onConnected());
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // Only re-runs when roomId changes
}

useEffectEvent DO / DON'T:

DO DON'T Why
Use for event-like callbacks inside Effects (notifications, logging, analytics) Use just to silence exhaustive-deps lint warnings Hiding real dependencies causes bugs
Treat it as a "snapshot" of latest values for Effect callbacks Call it outside of an Effect's context It's specifically designed for Effect event callbacks
Let the Effect depend only on values that should trigger re-setup Add every referenced value to deps even if it shouldn't re-trigger That's the problem useEffectEvent solves

cacheSignal API

For React Server Components — provides an AbortSignal that fires when cache() lifetime ends.

import { cache, cacheSignal } from 'react';

const dedupedFetch = cache(fetch);

async function UserProfile({ userId }) {
  const user = await dedupedFetch(
    `/api/users/${userId}`,
    { signal: cacheSignal() } // AbortSignal — cancels if render aborts
  );
  return <div>{user.name}</div>;
}

Signal fires when: render completes successfully, render is aborted, or render fails.


Partial Pre-rendering

Pre-render static parts on the server, resume later for dynamic content:

import { prerender, resume } from 'react-dom/server';

// Step 1: Pre-render static shell
const { prelude, postponed } = await prerender(<App />, {
  signal: controller.signal,
});
// Send prelude to client/CDN immediately

// Step 2: Resume with dynamic content later
const resumeStream = await resume(<App />, postponedState);

Server Components

React Server Components (RSC) are stable in React 19. They render on the server and send zero JavaScript to the client.

Benefits:

  • Zero bundle size — server components add nothing to client JS
  • Direct backend access — query databases/APIs directly
  • Automatic code splitting — client components are auto-split
  • Improved SEO — content rendered on server

Server Component Example

// This is a Server Component (default — no directive needed)
async function BlogPosts() {
  const posts = await db.posts.findMany(); // Direct DB query

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <LikeButton postId={post.id} /> {/* Client component for interactivity */}
        </article>
      ))}
    </div>
  );
}

Server Actions

// Server Action — runs on server, called from client
async function createPost(formData: FormData) {
  'use server';
  const title = formData.get('title');
  const content = formData.get('content');
  await db.posts.create({ data: { title, content } });
  revalidatePath('/posts');
}

// Client Component using Server Action
function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </form>
  );
}

Server Component DO / DON'T

DO DON'T Why
Keep components as Server Components by default Add 'use client' everywhere "just in case" Every 'use client' adds to the JS bundle
Access databases/files/APIs directly in Server Components Create API routes just to fetch data for rendering Direct access is the entire point of Server Components
Use async/await in Server Components Use useEffect + useState to fetch data in Server Components Server Components support top-level async
Add 'use client' only for: hooks, event handlers, browser APIs Add 'use client' because the component imports something The directive is about interactivity, not imports
Pass serializable data from Server → Client components Pass functions, classes, or DOM nodes as props to Client Components Only JSON-serializable values cross the server/client boundary
Use 'use server' on functions that mutate data Use 'use server' on functions that just read data Reading should happen in Server Components directly
Import Client Components inside Server Components Import Server Components inside Client Components Client → Server import is not supported; pass as children instead
Render Client Components as children: <ServerComp><ClientComp /></ServerComp> Try to use useState or useEffect in Server Components Server Components cannot use React hooks

Server vs Client Component Rules (Summary)

Server Components CAN: access backend resources directly, use async/await, import and render Client Components.

Server Components CANNOT: use browser APIs (window, document), use React hooks (useState, useEffect, etc.), use event handlers (onClick, onChange), use context providers.

To make a Client Component: add 'use client' at the top of the file:

'use client';
import { useState } from 'react';
// Hooks, browser APIs, event handlers allowed here

Common Anti-Patterns to Fix

When reviewing or refactoring React code, watch for these patterns and replace them:

Anti-Pattern 1: Manual Form State Boilerplate

// ---- ANTI-PATTERN: 6 useState calls for one form ----
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);

const handleSubmit = async (e) => {
  e.preventDefault();
  setIsSubmitting(true);
  // ... try/catch/finally with manual cleanup
};

// ---- FIX: useActionState ----
const [state, formAction, isPending] = useActionState(submitFn, null);
// One hook. Pending tracked automatically. Form auto-resets.

Anti-Pattern 2: useEffect for Data Fetching

// ---- ANTI-PATTERN: useEffect + useState for fetching ----
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  let cancelled = false;
  fetchData().then(d => { if (!cancelled) setData(d); setLoading(false); });
  return () => { cancelled = true; };
}, []);

// ---- FIX: Server Component (if using RSC framework) ----
async function DataDisplay() {
  const data = await fetchData(); // Direct fetch in Server Component
  return <div>{data.value}</div>;
}

// ---- FIX: use() + Suspense (if client-side) ----
function DataDisplay({ dataPromise }) {
  const data = use(dataPromise);
  return <div>{data.value}</div>;
}

Anti-Pattern 3: forwardRef Wrapper

// ---- ANTI-PATTERN: unnecessary forwardRef ----
const Input = forwardRef((props, ref) => {
  return <input ref={ref} className="input" {...props} />;
});

// ---- FIX: ref is a regular prop ----
function Input({ ref, ...props }) {
  return <input ref={ref} className="input" {...props} />;
}

Anti-Pattern 4: Context.Provider Nesting

// ---- ANTI-PATTERN: .Provider wrapper ----
<ThemeContext.Provider value={theme}>
  <AuthContext.Provider value={auth}>
    <App />
  </AuthContext.Provider>
</ThemeContext.Provider>

// ---- FIX: Context directly ----
<ThemeContext value={theme}>
  <AuthContext value={auth}>
    <App />
  </AuthContext>
</ThemeContext>

Anti-Pattern 5: useEffect for Derived/Computed State

// ---- ANTI-PATTERN: useEffect to compute derived state ----
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() => {
  setFilteredItems(items.filter(i => i.active));
}, [items]);

// ---- FIX: Compute during rendering ----
const [items, setItems] = useState([]);
const filteredItems = items.filter(i => i.active);
// No Effect needed. Recalculates automatically when items changes.
// Use useMemo if the computation is expensive:
// const filteredItems = useMemo(() => items.filter(i => i.active), [items]);

Anti-Pattern 6: Effect Dependencies That Shouldn't Re-trigger

// ---- ANTI-PATTERN: Effect re-runs on every prop change ----
useEffect(() => {
  const ws = connect(roomId);
  ws.onMessage = (msg) => logMessage(userId, msg); // userId causes re-connect
  return () => ws.close();
}, [roomId, userId]); // userId shouldn't re-connect!

// ---- FIX: useEffectEvent (React 19.2) ----
const onMessage = useEffectEvent((msg) => {
  logMessage(userId, msg); // Always sees latest userId
});

useEffect(() => {
  const ws = connect(roomId);
  ws.onMessage = onMessage;
  return () => ws.close();
}, [roomId]); // Only roomId triggers re-connect

Complete Migration Examples

Full Form Migration

BEFORE (React 18) — manual everything:

function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify({ name, email, message })
      });
      if (!response.ok) throw new Error('Failed');
      setSuccess(true);
      setName('');
      setEmail('');
      setMessage('');
    } catch (err) {
      setError(err.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
      <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
      <textarea value={message} onChange={e => setMessage(e.target.value)} placeholder="Message" />
      <button type="submit" disabled={isSubmitting}>Submit</button>
      {error && <p className="error">{error}</p>}
      {success && <p className="success">Sent!</p>}
    </form>
  );
}

AFTER (React 19) — useActionState, auto-reset:

import { useActionState } from 'react';

async function submitContact(prevState, formData) {
  'use server';
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');

  try {
    await db.messages.create({ name, email, message });
    return { success: true };
  } catch (error) {
    return { error: 'Failed to send' };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitContact, null);

  return (
    <form action={formAction}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      <textarea name="message" placeholder="Message" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Sending...' : 'Submit'}
      </button>
      {state?.error && <p className="error">{state.error}</p>}
      {state?.success && <p className="success">Sent!</p>}
    </form>
  );
}
// 6 useState calls → 1 useActionState. Form auto-resets on success.

Codemod Commands

Run All React 19 Codemods at Once

npx codemod@latest react/19/migration-recipe
npx codemod@latest react/19/migration-recipe --target ./src

Individual Codemods

Codemod What It Does
react/remove-context-provider Context.ProviderContext
react/remove-forward-ref Removes forwardRef, ref becomes regular prop
react/use-context-hook useContextuse
react/replace-use-form-state useFormStateuseActionState
react/replace-reactdom-render ReactDOM.rendercreateRoot
react/replace-string-ref String refs → callback refs
react/replace-act-import act import: react-dom/test-utils → react
react/create-element-to-jsx createElement → JSX
react/update-react-imports import React → named imports

Usage

npx codemod react/remove-context-provider --target <path>
npx codemod react/remove-forward-ref --target <path>
npx codemod react/use-context-hook --target <path>
npx codemod react/replace-use-form-state --target <path>
npx codemod react/replace-reactdom-render --target <path>
npx codemod react/replace-string-ref --target <path>
npx codemod react/replace-act-import --target <path>
npx codemod react/create-element-to-jsx --target <path>
npx codemod react/update-react-imports --target <path>

Legacy Codemods (for older upgrades)

npx codemod react/React-PropTypes-to-prop-types --target <path>  # React 15.5+
npx codemod react/rename-unsafe-lifecycles --target <path>        # React 16.3+
npx codemod react/pure-component --target <path>                  # React 16.8+
npx codemod react/manual-bind-to-arrow --target <path>            # React 18+

Breaking Changes

Removed APIs — Complete List

Removed API Replacement Will It Throw?
ReactDOM.render createRoot Yes — runtime error
ReactDOM.hydrate hydrateRoot Yes — runtime error
String refs (ref="string") useRef or callback refs Yes — runtime error
React.PropTypes prop-types package or TypeScript Yes — undefined
React.createFactory JSX or createElement Yes — undefined
element.ref access Not accessible Yes — warning then error
useFormState (react-dom) useActionState (react) Yes — import error
react-dom/test-utils (most APIs) import { act } from 'react' Yes — import error

TypeScript Setup for React 19

npm install --save-dev @types/react@^19 @types/react-dom@^19

Key type changes:

  • Refs can be passed as regular props (no forwardRef type wrapper needed)
  • useActionState has full generic typing
  • use() infers return type from Promise/Context
  • Metadata elements (<title>, <meta>) are valid JSX

ESLint Setup for React 19

npm install --save-dev eslint-plugin-react-hooks@latest

The updated plugin understands useEffectEvent and won't flag it as a missing dependency.


Migration Checklist

Follow this order when migrating a project to React 19:

[ ] 1.  Update React packages to v19
[ ] 2.  Run: npx codemod@latest react/19/migration-recipe --target ./src
[ ] 3.  Update @types/react and @types/react-dom to v19
[ ] 4.  Update eslint-plugin-react-hooks to latest
[ ] 5.  Fix any remaining ReactDOM.render → createRoot
[ ] 6.  Fix any remaining string refs → useRef
[ ] 7.  Remove forwardRef wrappers → ref as regular prop
[ ] 8.  Replace Context.Provider → Context directly
[ ] 9.  Migrate useContext → use() where conditional reading helps
[ ] 10. Convert manual form state → useActionState
[ ] 11. Add useOptimistic where instant feedback improves UX
[ ] 12. Remove react-helmet → native <title> and <meta>
[ ] 13. Test all forms, actions, and Server Components
[ ] 14. Review for any remaining deprecated API usage

Quick Reference Card

New Hooks

Hook Purpose Import Version
useActionState Form submission state + pending react 19.0
useOptimistic Optimistic UI with auto-rollback react 19.0
useFormStatus Read parent form status (no prop drill) react-dom 19.0
use() Read Promise/Context conditionally react 19.0
useEffectEvent Non-reactive Effect event callbacks react 19.2

New Components & APIs

Name Purpose Version
<Activity> Show/hide UI preserving state 19.2
cacheSignal() AbortSignal for RSC cache lifetime 19.2
prerender() Partial pre-rendering on server 19.2
resume() Resume SSR stream after prerender 19.2

Decision Tree: Which Hook to Use

Need to handle form submission?
  → useActionState (manages state + pending + error)

Need instant UI feedback during async operation?
  → useOptimistic (auto-rollback on error)

Need form pending state in a child component?
  → useFormStatus (reads parent form, no prop drilling)

Need to read context conditionally or read a Promise?
  → use() (works in if/loops, integrates with Suspense)

Need to reference latest props/state from inside an Effect without re-triggering?
  → useEffectEvent (19.2) (breaks false dependencies)

Need to show/hide UI without losing state?
  → <Activity> (19.2) (preserves state when hidden)

Best Practices Summary

  • Default to Server Components; add 'use client' only when needed
  • Use useActionState for all form submissions — it replaces 3-5 manual useState calls
  • Use useOptimistic alongside useActionState for instant feedback
  • Use useFormStatus to eliminate isPending prop drilling
  • Use use() for conditional context reading and Promise-based data loading
  • Use useEffectEvent for Effect callbacks that shouldn't trigger re-subscription
  • Use <Activity> instead of conditional rendering when state preservation matters
  • Use native <title> and <meta> — no helmet library needed
  • Pass ref as a regular prop — delete all forwardRef wrappers
  • Use <Context value={...}> directly — delete all .Provider usage
  • Run the migration recipe codemod first, then fix remaining issues manually
Weekly Installs
2
GitHub Stars
1
First Seen
2 days ago
Installed on
amp2
cline2
openclaw2
opencode2
cursor2
kimi-cli2