Back to blog

React 19 Server Components in practice

January 20, 2026•Updated on September 5, 2026•Dedimarco
react javascript web server-components
React 19 Server Components in practice

In short: As of September 5, 2026, React Server Components let you render components before the client bundle, access server-side data, and reduce JavaScript sent to the browser. Adopt them gradually in a compatible framework, while keeping Client Components for state, effects, and events. Next.js is not required, but a suitable RSC integration is.

React 19 has been stable since December 5, 2024. It brings together Server Components, Actions, and APIs for asynchronous flows. The goal is not to move everything to the server, but to separate data rendering from interactivity clearly.

What are React Server Components?

Server Components change how React distributes rendering. Unlike classic components executed in the browser, they are rendered before the client bundle, at build time or for each request. The official Server Components documentation states that their own code and server-only dependencies are not sent to the browser.

What you need to know

A Server Component:

  • Has no lifecycle (no useEffect, useState, useContext)
  • Has no access to browser APIs (DOM, window, localStorage)
  • Can directly access the database, file system, or server environment variables
  • Adds zero JavaScript weight to the client bundle

Server Components are the default behavior in some compatible frameworks. To create a Client Component, add the 'use client' directive at the top of the file. Next.js is not mandatory, but React still requires an RSC integration supplied by a framework or bundler.

Component tree architecture

A typical pattern combines both types:

// app/dashboard/page.tsx — Server Component (default)
export default async function Dashboard() {
  // This call runs on the server, no client-side fetch
  const stats = await db.getDashboardStats();
  const recentOrders = await db.getRecentOrders();

  return (
    <main>
      <h1>Dashboard</h1>
      {/* Server Component: no JS sent to client */}
      <StatsGrid stats={stats} />

      {/* Client Component: user interaction required */}
      <OrderSearchForm />
      <OrderTable initialData={recentOrders} />
    </main>
  );
}
// components/OrderSearchForm.tsx
'use client'; // Required directive for Client Components

import { useState } from 'react';

export function OrderSearchForm() {
  const [query, setQuery] = useState('');
  // ... interactive logic
}

How do Actions simplify form handling?

React 19 introduces Actions, a standardized way to handle asynchronous operations triggered by users. Gone is the useTransition + useState + error handling boilerplate.

Server Functions with use server

Async functions marked with the 'use server' directive execute on the server when called from the client. React documentation calls them Server Functions; a Server Function used by a form action is a Server Action:

// app/actions/contact.ts
'use server';

import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitContactAction(
  prevState: { error?: string; success?: boolean },
  formData: FormData,
) {
  const validated = contactSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  });

  if (!validated.success) {
    return { error: 'Invalid data' };
  }

  try {
    await db.contacts.create(validated.data);
    await sendNotificationEmail(validated.data);
    return { success: true };
  } catch (e) {
    return { error: 'Failed to send' };
  }
}

Forms with useActionState

The useActionState hook manages form state (pending, errors, return value) in one place:

// components/ContactForm.tsx
'use client';

import { useActionState } from 'react';
import { submitContactAction } from '@/app/actions/contact';

export function ContactForm() {
  const [state, action, isPending] = useActionState(submitContactAction, {
    error: undefined,
    success: undefined,
  });

  if (state.success) {
    return <p className="success">Message sent successfully!</p>;
  }

  return (
    <form action={action}>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" required disabled={isPending} />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" required disabled={isPending} />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea id="message" name="message" required disabled={isPending} />
      </div>

      {state.error && <p className="error">{state.error}</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

useActionState replaces the older useFormState. It accepts an Action and initial state, then returns the current state, the Action to bind to the form, and the isPending boolean.

The useFormStatus hook

To access submission state from a child component of the form, use useFormStatus (from react-dom):

'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? <span className="spinner" /> : 'Submit'}
    </button>
  );
}

This hook is scoped to the nearest form, avoiding prop drilling.

How does use() read resources during render?

Unlike conventional hooks, the use() API can be called conditionally and inside a loop. It reads resources such as a Promise or Context during render and integrates with Suspense.

With Promises

// Server Component
async function UserProfile({ userId }: { userId: string }) {
  const userDataPromise = fetchUserData(userId);

  return <UserDetails promise={userDataPromise} />;
}

// Client Component
('use client');
import { use } from 'react';

function UserDetails({ promise }: { promise: Promise<UserData> }) {
  // Suspends rendering until the promise resolves
  const userData = use(promise);

  return (
    <div>
      <h2>{userData.name}</h2>
      <p>{userData.email}</p>
    </div>
  );
}

With Context

'use client';
import { use, createContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton({ children }: { children: React.ReactNode }) {
  // use() replaces useContext, with conditional support
  const theme = use(ThemeContext);

  return <button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>{children}</button>;
}

How does useOptimistic provide instant feedback?

The useOptimistic hook provides an optimistic interface update while an Action is running. React immediately shows the temporary value, then returns to the received value if the operation fails or when the current value changes.

'use client';

import { useOptimistic, useTransition } from 'react';

interface Comment {
  id: string;
  text: string;
  author: string;
}

function CommentSection({ initialComments }: { initialComments: Comment[] }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    initialComments,
    (state, newComment: Comment) => [...state, newComment],
  );

  const [isPending, startTransition] = useTransition();

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;
    const tempComment: Comment = {
      id: crypto.randomUUID(),
      text,
      author: 'You',
    };

    startTransition(async () => {
      // Show the comment immediately
      addOptimisticComment(tempComment);

      // Server call
      await addComment({ text });
    });
  }

  return (
    <div>
      <ul>
        {optimisticComments.map((comment) => (
          <li key={comment.id}>
            <strong>{comment.author}:</strong> {comment.text}
          </li>
        ))}
      </ul>

      <form action={handleSubmit}>
        <textarea name="text" required />
        <button type="submit" disabled={isPending}>
          Comment
        </button>
      </form>
    </div>
  );
}

Which form approach should you choose?

Criteria Classic useState React Hook Form React 19 (Actions)
Boilerplate High Moderate Minimal
Pending/error handling Manual Built-in Built-in (useActionState)
Optimistic updates Manual No Yes (useOptimistic)
Validation External Built-in + schema Server-side
Re-render performance Every keystroke Controlled Minimal (server-driven)
Progressive enhancement No No Yes

Which best practices should you follow?

  1. Keep Server Components as default: only add 'use client' when you need interactivity (state, effects, events).

  2. Place 'use client' directives at the leaves: parent components remain Server Components as long as they don’t pass callbacks or interactive JSX.

  3. Always validate server-side: Server Actions don’t replace server validation. Client data is never trustworthy.

  4. Use useOptimistic for frequent interactions: likes, comments, list updates — anything that benefits from instant feedback.

Conclusion

React 19 provides a stable foundation for Server Components, Actions, and the use, useActionState, useFormStatus, and useOptimistic APIs. However, the internal APIs used by RSC frameworks and bundlers may still change between minor releases, so pin the React version if you build that integration yourself.

For an existing application, begin with the official React 19 upgrade guide. Then introduce Server Components at data-reading boundaries and keep interactivity in small Client Components. This gradual migration is safer than a complete rewrite.

Sources

Frequently asked questions

What is a React Server Component?

A React Server Component is rendered before the interface reaches the browser, in an environment separate from the client. It can read server-side data, and its own code is not added to the browser's JavaScript bundle.

Is Next.js required for React Server Components?

No. React does not require Next.js, but Server Components need an integration provided by a compatible framework or bundler. They can also run at build time without a permanent web server.

How do you migrate an application to React 19?

Start with React 18.3 to surface migration warnings, then follow the official React 19 guide and its codemods. Adopt Server Components gradually, without converting interactive components that depend on browser APIs.

What is the difference between Server Components and SSR?

SSR produces the initial HTML for an application that may then hydrate on the client. Server Components execute outside the client and pass their result to Client Components without adding their own code to the browser bundle.