Explore server components, reading promises with use, and React 19's optimistic updates.
Open this lesson in KodokonReact 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.
// 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>
);
}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.
'use client';
import { useState } from 'react';
export function LikeButton({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(count + 1)}>
{count} likes
</button>
);
}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.
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>
);
}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.
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.
});
}use API allowed to break?useOptimistic value when the async action finishes?