Claude Code for React 19: Actions, Transitions, and the New Compiler — Claude Skills 360 Blog
Blog / Development / Claude Code for React 19: Actions, Transitions, and the New Compiler
Development

Claude Code for React 19: Actions, Transitions, and the New Compiler

Published: July 10, 2026
Read time: 8 min read
By: Claude Skills 360

React 19 ships four things that change how you write components: Actions (async functions that handle form submissions), useActionState (replaces the verbose useState pattern for form state), useOptimistic (built-in optimistic updates), and the React Compiler (automatic memoization). Claude Code generates React 19 patterns correctly — the new conventions are specific and the upgrade path from React 18 has gotchas.

This guide covers React 19 with Claude Code: Actions, useActionState, useOptimistic, the Compiler, and migration patterns.

React 19 Actions

Before React 19, handling form submissions required manual loading state, error state, and optimistic updates in useState. React 19’s Actions handle this pattern natively.

// React 18 pattern — verbose
function CommentForm({ postId }: { postId: string }) {
  const [content, setContent] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);
    
    try {
      await addComment(postId, content);
      setContent('');
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea value={content} onChange={e => setContent(e.target.value)} />
      {error && <p>{error}</p>}
      <button disabled={isLoading}>{isLoading ? 'Posting...' : 'Post'}</button>
    </form>
  );
}

// React 19 — Actions handle this automatically
function CommentForm({ postId }: { postId: string }) {
  const [state, submitAction, isPending] = useActionState(
    async (prevState: ActionState, formData: FormData) => {
      const content = formData.get('content') as string;
      
      try {
        await addComment(postId, content);
        return { success: true, error: null };
      } catch (err) {
        return { success: false, error: (err as Error).message };
      }
    },
    { success: false, error: null }
  );
  
  return (
    <form action={submitAction}>  {/* form action — not onSubmit */}
      <textarea name="content" required />
      {state.error && <p role="alert">{state.error}</p>}
      <SubmitButton />  {/* Gets isPending from useFormStatus */}
    </form>
  );
}

// Separate component so useFormStatus works
function SubmitButton() {
  const { pending } = useFormStatus();  // Works within the form
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Posting...' : 'Post Comment'}
    </button>
  );
}

useOptimistic

Add a like button that updates instantly without waiting for
the server response.
// src/components/LikeButton.tsx
import { useOptimistic, startTransition } from 'react';

interface LikeState {
  liked: boolean;
  count: number;
}

export function LikeButton({ postId, initialLiked, initialCount }: {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}) {
  const [actualState, setActualState] = useState<LikeState>({
    liked: initialLiked,
    count: initialCount,
  });
  
  // useOptimistic: shows optimistic value while mutation is in-flight
  // Reverts to actualState if mutation fails
  const [optimisticState, setOptimisticState] = useOptimistic(
    actualState,
    (currentState, newLiked: boolean) => ({
      liked: newLiked,
      count: currentState.count + (newLiked ? 1 : -1),
    })
  );
  
  const handleLike = async () => {
    const newLiked = !optimisticState.liked;
    
    // Update optimistic state immediately
    startTransition(() => {
      setOptimisticState(newLiked);
    });
    
    // Perform actual mutation
    try {
      const result = newLiked
        ? await likePost(postId)
        : await unlikePost(postId);
      
      // Update actual state with server response
      setActualState({ liked: result.liked, count: result.count });
    } catch (error) {
      // useOptimistic automatically reverts on error
      console.error('Like failed:', error);
    }
  };
  
  return (
    <button
      onClick={handleLike}
      aria-label={optimisticState.liked ? 'Unlike' : 'Like'}
      aria-pressed={optimisticState.liked}
    >
      {optimisticState.liked ? '♥' : '♡'} {optimisticState.count}
    </button>
  );
}

The React Compiler

Our dashboard re-renders constantly because of missing
useMemo/useCallback. Can the React Compiler fix this?

The React Compiler automatically adds memoization — you no longer need to manually call useMemo and useCallback in most cases.

// Before React Compiler — manual memoization everywhere
function UserDashboard({ userId }: { userId: string }) {
  const user = useUser(userId);
  
  // Without useMemo, this recalculates on every render
  const stats = useMemo(() =>
    calculateStats(user.orders, user.preferences),
    [user.orders, user.preferences]
  );
  
  // Without useCallback, this is a new function on every render
  const handleExport = useCallback(() => {
    exportUserData(userId, stats);
  }, [userId, stats]);
  
  return <DashboardLayout stats={stats} onExport={handleExport} />;
}

// After React Compiler — memoization handled automatically
// The compiler analyzes dependencies and adds memoization where needed
function UserDashboard({ userId }: { userId: string }) {
  const user = useUser(userId);
  const stats = calculateStats(user.orders, user.preferences); // Compiler memoizes
  const handleExport = () => exportUserData(userId, stats);    // Compiler memoizes
  
  return <DashboardLayout stats={stats} onExport={handleExport} />;
}

// Cleaner, less boilerplate — same runtime behavior

Enable the compiler:

// babel.config.js (or vite.config.ts with babel plugin)
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // target: '19',  // Only needed when using React 18 compat mode
    }],
  ],
};
The React Compiler shows eslint errors in one of my components.
Why?

The compiler requires strict React rules — components it can’t safely optimize are skipped with a warning:

// Problem: mutating state directly (breaks compiler assumptions)
function BadComponent() {
  const [items, setItems] = useState([]);
  
  const addItem = (item) => {
    items.push(item);  // Direct mutation — compiler can't track this
    setItems(items);   // Passes same reference — React won't re-render
  };
}

// Fixed: immutable update
function GoodComponent() {
  const [items, setItems] = useState([]);
  
  const addItem = (item) => {
    setItems(prev => [...prev, item]);  // New array — traceable by compiler
  };
}

New use() Hook

We have a promise from the parent component.
Read it in a child component with the new use() hook.
// React 19: use() can unwrap promises and context
import { use, Suspense } from 'react';

// Parent creates the promise (in a Server Component or outside component tree)
function PostPage({ postId }: { postId: string }) {
  const postPromise = fetchPost(postId); // Don't await — pass the Promise
  
  return (
    <Suspense fallback={<PostSkeleton />}>
      <PostContent postPromise={postPromise} />
    </Suspense>
  );
}

// Child reads the promise — Suspense handles loading state
function PostContent({ postPromise }: { postPromise: Promise<Post> }) {
  const post = use(postPromise); // Suspends until promise resolves
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

// use() also works conditionally (unlike other hooks)
function UserProfile({ user }: { user: User | null }) {
  if (!user) return <SignInPrompt />;
  
  const theme = use(ThemeContext); // Can be called after a conditional!
  
  return <div style={{ color: theme.primary }}>{user.name}</div>;
}

For upgrading to React 19, the breaking changes are minimal: React.FC’s children prop handling changed, refs can now be passed as regular props (forwardRef is deprecated), and ReactDOM.render is fully removed. Claude Code handles the migration diff automatically.

For Next.js App Router with Server Components and React 19’s new patterns, see the Next.js App Router guide. For TanStack Query which integrates with React 19’s transitions for data fetching, see the TanStack Query guide. The Claude Skills 360 bundle includes React 19 upgrade skill sets. Start with the free tier to migrate your components to React 19 patterns.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free