Kodokon kodokon.com

Modern React: Server Components, use, useTransition, useOptimistic

Explore server components, reading promises with use, and React 19's optimistic updates.

11 min · 3 questions

Open this lesson in Kodokon

React Server Components (RSC) redraw the client-server boundary. A server component runs only on the server: its code never reaches the client bundle, it can be async, read the database or the file system directly, and its output is sent to the browser as a serialized stream (the RSC payload) that React merges with the client tree. In exchange, it has no state, no effects, and no access to the browser: no useState, no useEffect, no event handlers. It never re-renders on the client.

JSX
// Server component: never sent to the client.
export default async function NotesPage() {
  const notes = await db.notes.findAll();
  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id}>{note.title}</li>
      ))}
    </ul>
  );
}
An async server component queries the database directly.

Interactivity lives in client components, marked by the 'use client' directive at the top of the file. This directive does not mark "a client component" but a boundary: everything that module imports also moves to the client side. Props crossing the boundary must be serializable: plain objects, arrays, promises... but never ordinary functions or class instances.

JSX
'use client';

import { useState } from 'react';

export function LikeButton({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return (
    <button onClick={() => setCount(count + 1)}>
      {count} likes
    </button>
  );
}
The 'use client' directive delimits the interactive zone.

React 19 introduces use, an API that reads the value of a promise or a context. Unlike hooks, use can be called inside a condition or a loop. Given a promise, it suspends the component until resolution, relying on the nearest Suspense boundary. The idiomatic pattern: a server component creates the promise without awaiting it, passes it as a prop, and a client component consumes it with use: the server starts streaming the page without waiting for the data.

JSX
import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);
  return comments.map((c) => (
    <p key={c.id}>{c.text}</p>
  ));
}

function Page({ commentsPromise }) {
  return (
    <Suspense fallback={<Spinner />}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}
use suspends the component until the promise resolves.

On the interaction side, useTransition marks an update as non-urgent and exposes isPending so you can show a subtle indicator without blocking input. useOptimistic completes the duo for network mutations: you immediately display the hoped-for state while an async action runs; when the action ends (success or failure), React drops the optimistic value and shows the real state again, the one from the source of truth.

JSX
const [optimisticLikes, addOptimisticLike] =
  useOptimistic(likes, (count, delta) => count + delta);

function handleLike() {
  startTransition(async () => {
    addOptimisticLike(1);
    await saveLike();
    // At the end, React shows the real state again.
  });
}
The interface reacts immediately, the server confirms afterwards.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What can a server component not do?
    • Be declared async and await a request
    • Read the database directly
    • Use useState and event handlers
    • Render client components as children
  2. Which rule of hooks is the use API allowed to break?
    • Being called outside a component
    • Being called inside a condition or a loop
    • Being called after an early return of the module
    • Being called inside a class
  3. What happens to the useOptimistic value when the async action finishes?
    • It becomes the component's permanent state
    • It is dropped and the real state takes over again
    • It is merged with the real state
    • It is kept until the next manual render