MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React 19 RC: Actions, Compiler, and use() Hook

React 19 release candidate: Server Actions, React Compiler, and new hooks.

ReactReact 19CompilerFrontend

By MinhVo

Introduction

React 19 represents the most significant evolution of the React framework since the introduction of hooks in React 16.8. The release candidate introduces three transformative features: Server Actions for seamless client-server data mutations, the React Compiler for automatic performance optimization, and the new use() hook for reading resources during render. Together, these features fundamentally change how we think about building React applications, moving from a purely client-side paradigm to a unified full-stack model.

The React team has been working toward this vision for years. Server Components laid the groundwork in React 18, but React 19 completes the picture by enabling mutations without client-side JavaScript. The React Compiler (formerly React Forget) eliminates the need for manual memoization with useMemo, useCallback, and React.memo, automatically optimizing re-renders at compile time. The use() hook introduces a new primitive for reading async resources that integrates with Suspense in ways that were previously impossible.

This guide explores each of these features in depth, examining how they work internally, when to use them, and how they change established React patterns. Whether you're building a new application or upgrading an existing one, understanding React 19 is essential for staying at the forefront of frontend development.

React 19 features overview

Understanding React 19: Core Concepts

Server Actions

Server Actions are functions that run on the server and can be called directly from client components. They're defined with the "use server" directive and handle form submissions, data mutations, and any operation that needs server-side execution. Unlike traditional API endpoints, Server Actions are collocated with components, automatically handle serialization, and integrate with React's transition system for pending states and error boundaries.

The key insight behind Server Actions is that most web applications follow a pattern: the user performs an action, the server processes it, and the UI updates. Server Actions codify this pattern into a first-class React primitive, eliminating the boilerplate of defining API routes, writing fetch calls, managing loading states, and handling optimistic updates.

React Compiler

The React Compiler is a build-time tool that automatically memoizes components, hooks, and expressions. It analyzes your code's data flow and inserts memoization only where needed, following React's rules of immutability and pure rendering. This means developers can write simpler code without worrying about performanceβ€”the compiler handles optimization automatically.

The compiler works by transforming your code at build time, inserting useMemo, useCallback, and React.memo equivalents where it detects that values haven't changed. It respects React's rules: if a component's inputs haven't changed, it won't re-render. If a callback depends on the same state, it won't be recreated.

The use() Hook

The use() hook is a new React primitive that reads a resource during render, similar to how you'd read a promise value. Unlike useEffect or useState, use() can be called conditionally and inside loops. It integrates with Suspense for loading states and Error Boundaries for error handling. It can read promises, context, and any "thenable" value.

Architecture and Design Patterns

Server-Client Boundary

React 19 introduces a clear architectural pattern for server and client code. Server Components render on the server and can directly access databases, file systems, and environment variables. Client Components run in the browser and handle interactivity. Server Actions bridge the two, providing a secure way for client code to trigger server-side mutations.

Client (Browser)                    Server (Node.js)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client         β”‚  Server Action β”‚  Server         β”‚
β”‚  Components     β”‚ ──────────────>β”‚  Components     β”‚
β”‚  (interactivity)β”‚  (form submit) β”‚  (data access)  β”‚
β”‚                 β”‚ <──────────────│                 β”‚
β”‚  Optimistic UI  β”‚  Response      β”‚  Database/API   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow with use() and Suspense

The use() hook creates a new data flow pattern where async resources are read during render rather than in effects. This integrates naturally with Suspense boundaries for granular loading states.

import { use, Suspense } from 'react';
 
function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <div>{user.name}</div>;
}
 
function App() {
  const userPromise = fetchUser();
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Form Actions Pattern

Server Actions integrate with HTML forms through the action prop, enabling progressive enhancement. Forms work without JavaScript and progressively enhance with client-side interactivity.

Step-by-Step Implementation

Setting Up Server Actions

Server Actions are defined in server-side files using the "use server" directive. They can be imported into both server and client components.

// app/actions/todo.ts
"use server"
 
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
import { z } from "zod";
 
const TodoSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().max(1000).optional(),
});
 
export async function createTodo(formData: FormData) {
  const validated = TodoSchema.parse({
    title: formData.get("title"),
    description: formData.get("description"),
  });
 
  await db.todo.create({
    data: {
      title: validated.title,
      description: validated.description ?? "",
      completed: false,
    },
  });
 
  revalidatePath("/todos");
}
 
export async function toggleTodo(id: string) {
  const todo = await db.todo.findUnique({ where: { id } });
  if (!todo) throw new Error("Todo not found");
 
  await db.todo.update({
    where: { id },
    data: { completed: !todo.completed },
  });
 
  revalidatePath("/todos");
}
 
export async function deleteTodo(id: string) {
  await db.todo.delete({ where: { id } });
  revalidatePath("/todos");
}

Using Server Actions in Components

Server Actions can be used with forms directly or through the useActionState hook for more control:

"use client"
 
import { useActionState } from "react";
import { createTodo } from "@/app/actions/todo";
 
function TodoForm() {
  const [state, formAction, isPending] = useActionState(createTodo, null);
 
  return (
    <form action={formAction}>
      <input name="title" placeholder="Todo title" required />
      <textarea name="description" placeholder="Description" />
      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Add Todo"}
      </button>
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  );
}

Optimistic Updates with useOptimistic

React 19 introduces useOptimistic for instant UI feedback while server mutations are in progress:

"use client"
 
import { useOptimistic, useTransition } from "react";
import { toggleTodo } from "@/app/actions/todo";
 
function TodoItem({ todo }) {
  const [optimisticTodo, setOptimisticTodo] = useOptimistic(
    todo,
    (current, newState) => ({ ...current, ...newState })
  );
 
  async function handleToggle() {
    setOptimisticTodo({ completed: !optimisticTodo.completed });
    await toggleTodo(todo.id);
  }
 
  return (
    <li className={optimisticTodo.completed ? "completed" : ""}>
      <span>{optimisticTodo.title}</span>
      <button onClick={handleToggle}>
        {optimisticTodo.completed ? "Undo" : "Complete"}
      </button>
    </li>
  );
}

React development workflow

Real-World Use Cases

Use Case 1: Form-Heavy Applications

Server Actions eliminate the boilerplate of building forms with API routes. A registration form that previously required an API route, fetch call, loading state management, and error handling now needs just a Server Action and a form element. The progressive enhancement means the form works even without JavaScript, improving accessibility and reliability.

Use Case 2: Data Dashboards

Dashboards that display server data benefit from use() combined with Suspense. Each widget can independently suspend while its data loads, providing granular loading states rather than a single loading spinner for the entire page.

Use Case 3: E-Commerce Checkout

Checkout flows with multiple steps, address validation, and payment processing use Server Actions for each step. The useFormStatus hook provides per-button loading states, and useOptimistic shows cart updates instantly while the server processes changes.

Best Practices for Production

  1. Colocate Server Actions with components: Keep mutations close to the components that trigger them. This improves maintainability and makes data flow easier to follow.

  2. Validate inputs with Zod: Always validate Server Action inputs on the server, even if you validate on the client. Client validation can be bypassed; server validation cannot.

  3. Use useActionState for complex forms: For forms that need error messages, validation feedback, or multi-step flows, useActionState provides more control than raw form actions.

  4. Leverage Suspense boundaries strategically: Place Suspense boundaries where you want loading states to appear. A boundary at the page level shows a full-page loader; boundaries around individual components show granular loaders.

  5. Don't overuse useOptimistic: Optimistic updates are powerful but add complexity. Use them for user-initiated actions where instant feedback matters (likes, toggles, reordering), not for data that the user doesn't interact with directly.

  6. Keep Server Actions focused: Each Server Action should do one thing. Don't create a single updateEverything action. Instead, create specific actions like updateUserName, updateEmail, updatePassword.

Common Pitfalls and Solutions

PitfallImpactSolution
Using "use server" in client componentsRuntime errorDefine actions in separate server files
Not handling Server Action errorsSilent failuresWrap actions in try/catch, return error states
Calling actions outside transitionsNo pending stateUse useTransition or useActionState
Over-fetching with use()Unnecessary requestsMemoize promises with useMemo
Missing Suspense boundaries for use()Unhandled loading statesWrap use() consumers in Suspense

Performance Optimization

The React Compiler automatically optimizes re-renders by analyzing data flow at build time. To ensure the compiler can optimize effectively:

// Good: Compiler can optimize this
function ItemList({ items }) {
  const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
  return sorted.map(item => <Item key={item.id} item={item} />);
}
 
// Also good: Explicit memoization when needed for non-compiler environments
import { useMemo, useCallback } from 'react';
 
function SearchResults({ query, onSelect }) {
  const filtered = useMemo(
    () => items.filter(item => item.name.includes(query)),
    [query, items]
  );
  const handleClick = useCallback((id) => onSelect(id), [onSelect]);
  return filtered.map(item => <Result key={item.id} item={item} onClick={handleClick} />);
}

Comparison with Alternatives

FeatureReact 19 Server ActionsNext.js API RoutestRPCGraphQL
ColocationWith componentsSeparate filesSeparate filesSchema files
Type safetyManual (Zod)ManualAutomaticSchema-derived
Progressive enhancementBuilt-inNoNoNo
Bundle sizeZero JS for actionsClient bundleClient bundleClient bundle
ComplexityLowMediumMediumHigh

Advanced Patterns

Parallel Data Fetching with use()

Fetch multiple independent resources simultaneously:

import { use, Suspense } from 'react';
 
function Dashboard({ userId }) {
  const userPromise = fetchUser(userId);
  const statsPromise = fetchStats(userId);
  const notificationsPromise = fetchNotifications(userId);
 
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile promise={userPromise} />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <UserStats promise={statsPromise} />
      </Suspense>
      <Suspense fallback={<NotifSkeleton />}>
        <Notifications promise={notificationsPromise} />
      </Suspense>
    </div>
  );
}

Server Action with useTransition for Full Control

"use client"
 
import { useTransition } from "react";
import { submitOrder } from "@/app/actions/orders";
 
function OrderButton({ items }) {
  const [isPending, startTransition] = useTransition();
 
  function handleClick() {
    startTransition(async () => {
      const result = await submitOrder(items);
      if (result.success) {
        router.push(`/orders/${result.orderId}`);
      }
    });
  }
 
  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? "Placing order..." : "Place Order"}
    </button>
  );
}

Testing Strategies

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoForm } from './TodoForm';
 
// Mock server actions
jest.mock('@/app/actions/todo', () => ({
  createTodo: jest.fn(),
}));
 
test('submits form with correct data', async () => {
  const user = userEvent.setup();
  render(<TodoForm />);
 
  await user.type(screen.getByPlaceholderText('Todo title'), 'New todo');
  await user.click(screen.getByText('Add Todo'));
 
  await waitFor(() => {
    expect(screen.getByText('Creating...')).toBeInTheDocument();
  });
});
 
test('displays error on failed submission', async () => {
  const { createTodo } = require('@/app/actions/todo');
  createTodo.mockResolvedValue({ error: 'Title too long' });
 
  const user = userEvent.setup();
  render(<TodoForm />);
  await user.type(screen.getByPlaceholderText('Todo title'), 'x'.repeat(201));
  await user.click(screen.getByText('Add Todo'));
 
  await waitFor(() => {
    expect(screen.getByText('Title too long')).toBeInTheDocument();
  });
});

Future Outlook

React 19 is just the beginning of the server-first paradigm. Future React versions will expand Server Components with more granular streaming, improve the compiler's optimization capabilities, and add new primitives for common patterns like pagination and infinite scroll. The React team is also working on document metadata, improved Suspense for data fetching, and better DevTools integration for server-client debugging. The ecosystemβ€”Next.js, Remix, and other frameworksβ€”will continue to build higher-level abstractions on top of these primitives.

Error Handling with Server Actions

Server Actions need robust error handling since they execute on the server where network failures, validation errors, and database constraints can occur. Combine useActionState with try-catch for comprehensive error handling:

'use client';
import { useActionState } from 'react';
 
async function createOrder(
  prevState: OrderState,
  formData: FormData
): Promise<OrderState> {
  try {
    const items = JSON.parse(formData.get('items') as string);
    const total = items.reduce(
      (sum: number, item: CartItem) => sum + item.price * item.quantity,
      0
    );
 
    if (total <= 0) {
      return { error: 'Order total must be greater than zero', success: false };
    }
 
    const order = await db.order.create({
      data: {
        userId: formData.get('userId') as string,
        items: { create: items },
        total,
        status: 'pending',
      },
    });
 
    revalidatePath('/orders');
    return { success: true, orderId: order.id };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { error: error.errors[0].message, success: false };
    }
    console.error('Order creation failed:', error);
    return { error: 'An unexpected error occurred. Please try again.', success: false };
  }
}
 
export function OrderForm() {
  const [state, formAction, isPending] = useActionState(createOrder, {
    success: false,
  });
 
  return (
    <form action={formAction}>
      <input type="hidden" name="items" value={JSON.stringify(cartItems)} />
      <input type="hidden" name="userId" value={user.id} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Placing order...' : 'Place order'}
      </button>
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">Order #{state.orderId} placed!</p>}
    </form>
  );
}

Optimistic Updates with useOptimistic

React 19's useOptimistic hook provides a clean pattern for showing immediate UI feedback before the server confirms the action. This makes your application feel responsive even on slow networks:

'use client';
import { useOptimistic, useTransition } from 'react';
 
interface Todo {
  id: string;
  title: string;
  completed: boolean;
}
 
export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  );
 
  async function handleAddTodo(formData: FormData) {
    const title = formData.get('title') as string;
    const tempId = `temp-${Date.now()}`;
 
    addOptimisticTodo({
      id: tempId,
      title,
      completed: false,
    });
 
    // Server Action handles the real creation
    await createTodoAction(formData);
    // On success, the server re-renders with real data
    // The optimistic entry is replaced automatically
  }
 
  return (
    <div>
      <form action={handleAddTodo}>
        <input name="title" placeholder="New todo..." />
        <button type="submit">Add</button>
      </form>
      <ul>
        {optimisticTodos.map((todo) => (
          <li
            key={todo.id}
            style={{ opacity: todo.id.startsWith('temp') ? 0.6 : 1 }}
          >
            {todo.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

The optimistic entry appears instantly with reduced opacity to indicate pending state. When the server action completes and the component re-renders with fresh data, the optimistic entry is seamlessly replaced with the real one. If the action fails, the optimistic entry is removed and the UI reverts to the previous state.

React Compiler Adoption Strategy

The React Compiler works best when your codebase follows React's rules consistently. Start by running the compiler's ESLint plugin to identify violations in your existing code. Fix rule violations like side effects in render, conditional hook calls, and direct state mutations before enabling the compiler. The compiler will skip components that violate these rules, falling back to unoptimized rendering. Once your codebase passes the ESLint checks, enable the compiler in your build configuration and measure the performance improvement using React DevTools Profiler.

Conclusion

React 19 fundamentally changes how we build React applications by making the server a first-class citizen in the component model. Server Actions eliminate API route boilerplate and enable progressive enhancement. The React Compiler removes the burden of manual memoization. The use() hook provides a natural way to read async resources during render. Together, these features enable simpler, more performant, and more maintainable applications. Start adopting Server Actions for mutations today, enable the React Compiler in your build pipeline, and use use() with Suspense for data fetching patterns to build modern React applications that are ready for the future.