Server components, actions and the use API
Server and client components, streaming, useActionState and useOptimistic, reading promises with use, Suspense and error boundaries.
Server and client components
// app/guides/[slug]/page.tsx - a server component by default
async function GuidePage({ params }) {
const guide = await loadGuide(params.slug) // reads the database directly
return (
<article>
<h1>{guide.title}</h1>
<GuideBody markdown={guide.body} />
<LikeButton id={guide.id} initialLikes={guide.likes} />
</article>
)
}
// app/guides/[slug]/LikeButton.tsx
'use client' // this file and its imports run in the browser
import { useState } from 'react'
export function LikeButton({ id, initialLikes }) {
const [likes, setLikes] = useState(initialLikes)
return <button type="button" onClick={() => setLikes(n => n + 1)}>{likes} likes</button>
}| Capability | Server component | Client component |
|---|---|---|
| Reads a database, files or secrets | Yes | No |
| Uses state, refs or event handlers | No | Yes |
Can be declared async | Yes | No |
| Adds JavaScript to the bundle | No | Yes |
| Renders on the server as HTML | Yes | Yes, then hydrates |
| Imports the other kind | Can render a client component | Cannot import a server component, but can receive one as children |
Push the 'use client' boundary as far down the tree as it will go. Every module inside the boundary ships to the browser, so a single directive at the top of a page moves the whole page into the bundle.
Actions, useActionState and useOptimistic
// app/guides/actions.js
'use server' // every export becomes an endpoint the client may call
import { revalidatePath } from 'next/cache'
export async function likeGuide(previousState, formData) {
const id = String(formData.get('id') ?? '')
if (id === '') return { error: 'Missing id' }
try {
await db.guide.update({ where: { id }, data: { likes: { increment: 1 } } })
revalidatePath('/guides/' + id) // refresh the cached server output
return { error: null }
} catch {
return { error: 'Could not save the like' }
}
}'use client'
import { useActionState, useOptimistic } from 'react'
import { likeGuide } from '../actions'
export function LikeButton({ id, initialLikes }) {
const [likes, addOptimistic] = useOptimistic(initialLikes, (current, delta) => current + delta)
const [state, formAction, isPending] = useActionState(likeGuide, { error: null })
return (
<form
action={formAction}
onSubmit={() => addOptimistic(1)} // show the result before the server answers
>
<input type="hidden" name="id" value={id} />
<button type="submit" disabled={isPending}>
{likes} likes
</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
)
}useActionStategives a form submission a state, a stable action and a pending flag, with no manual loading state.useOptimisticrenders the expected outcome immediately and reverts automatically when the action fails or the server data arrives.- An action's return value is the next state, which is why returning
{ error }is the pattern rather than throwing for expected failures. revalidatePath(or its tag equivalent) is what makes the server-rendered output refresh after a mutation.
use, Suspense and error boundaries
import { Component, Suspense, use } from 'react'
// use reads a promise during render; the nearest Suspense boundary shows the fallback
function GuideTitle({ guidePromise }) {
const guide = use(guidePromise) // suspends until the promise resolves
return <h1>{guide.title}</h1>
}
function Page({ params }) {
// start the request before rendering the child, so the two overlap
const guidePromise = loadGuide(params.slug)
return (
<Suspense fallback={<TitleSkeleton />}>
<GuideTitle guidePromise={guidePromise} />
</Suspense>
)
}
// an error boundary catches what Suspense and the server cannot
class RouteErrorBoundary extends Component {
state = { error: null }
static getDerivedStateFromError(error) {
return { error }
}
render() {
if (this.state.error) return <ErrorPanel error={this.state.error} />
return this.props.children
}
}- Create the promise during render of a server component, not inside the component that reads it, so the request starts before the subtree suspends.
- A Suspense boundary shows the fallback for its whole subtree; place boundaries around the slow part rather than the whole page.
- Error boundaries must be class components or come from a library - there is no hook form. Catch render errors and nothing else: event handlers and async callbacks need their own try/catch.
- Streaming means the shell is sent first and slow parts follow, so users see meaningful HTML while the data is still arriving.
⚠️
A server action is a public HTTP endpoint with a generated name. Validate every field, check authorisation on the server, and never trust an id sent from the browser - anyone can call it directly with a crafted request.
FAQ
Do server components replace an API?
For your own UI, largely yes: the component can read data directly, with no round trip and no serialisation step. Keep a real API when other clients, third parties or webhooks need the same data.
Why can a client component not import a server component?
The import would have to run in the browser, where the server-only code and its secrets are unavailable. Pass the server-rendered output down as
children, which is serialised across the boundary.Related
Routing with React Router Production builds, environment config and deployment
Last refreshed 2026-09-18.