React Server Components 05 🧬 Async Components — Using await inside render for data fetching
An async component is a Server Component that is declared with the async keyword and uses await directly in its render body. It is the feature that makes the Server Component’s data access natural: the component reads the database, awaits the promise, and returns the JSX. The previous chapters covered the build-time and request-time models; this chapter covers the syntax that makes those models work, the Suspense boundaries that stream the result, and the hard rule that async components are server-only.
The defining constraint is that async components are only valid on the server. React throws a runtime error if an async function is used as a Client Component, and the error message is explicit: “An unknown Component is an async Client Component. Only Server Components can be async at the moment” . The client’s alternative is the use() API, which reads a promise without making the component async .
Key point: await in a Server Component suspends the rendering of that component until the promise resolves, and the Suspense boundary above it shows the fallback in the meantime . When the promise resolves, the rendered output is streamed to the client. A Client Component cannot be async; it reads the promise with use() instead .
Why async components require the server
The client limitation problem.
A Client Component runs in the browser during hydration, and the browser’s rendering model does not support an async render function. React’s error is unambiguous: “Only Server Components can be async at the moment” . The ESLint rule for the TanStack Router states the same constraint: “Async components are only valid inside createServerComponent(). Client components cannot be async (React throws at runtime)” . This is not a framework preference; it is a React runtime constraint.
The streaming problem.
A Server Component that awaits a promise suspends its own rendering, and the Suspense boundary above it streams the fallback to the browser. When the promise resolves, React resumes the render and streams the actual content . This is the mechanism that allows the page shell to appear immediately while the slower data loads in the background. The async component is the unit that participates in this streaming.
The use() alternative problem.
A Client Component cannot be async, but it can read a promise that was started on the server. The use() hook unwraps the promise and integrates with Suspense: the component suspends until the promise resolves, and the fallback is shown . The React documentation recommends passing the promise from the Server Component to the Client Component rather than creating it in the Client Component, because a promise created in the client is recreated on every render, while a promise passed from the server is stable .
a. The async Server Component and the data access
An async Server Component declares the async keyword and uses await in the render body. It can be the page, a layout, or any component rendered by a Server Component.
// app/posts/page.tsx (Server Component)
export default async function PostsPage() {
const posts = await db.posts.findMany()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
The PostsPage is an async function, and it awaits the database query during the render . The db module and the query never reach the client, because the Server Component’s code is not included in the client bundle. The rendered output—the list of titles—is serialized into the RSC Payload.
The async component can be nested inside a synchronous Server Component, which is the pattern that lets the static shell render immediately while the async data loads.
// app/page.tsx (Server Component)
import { Suspense } from 'react'
async function LatestPosts() {
const posts = await getPosts()
return <PostList posts={posts} />
}
export default function Page() {
return (
<div>
<header>My Blog</header>
<Suspense fallback={<PostsSkeleton />}>
<LatestPosts />
</Suspense>
<footer>© 2025</footer>
</div>
)
}
The Page is synchronous, and it renders immediately. The LatestPosts is async, and it suspends inside the Suspense boundary until the data resolves . The header and the footer appear on the first paint, and the posts stream in when they are ready.
b. The Suspense boundary and the streaming
The Suspense boundary is what makes the async component’s suspension visible. Without it, the entire page would wait for the async data before rendering anything.
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<Activity />
</Suspense>
</div>
)
}
async function Analytics() {
const data = await getAnalytics()
return <AnalyticsDisplay data={data} />
}
async function Activity() {
const data = await getActivity()
return <ActivityDisplay data={data} />
}
The two Suspense boundaries are independent. The Analytics and the Activity components load in parallel, and each one streams in as its data resolves . The h1 renders immediately, and the two sections show their skeletons until their data arrives.
The parallel fetching is the pattern that avoids the sequential waterfall. The Promise.all call starts multiple requests at the same time.
async function Page() {
const [user, posts, notifications] = await Promise.all([
db.users.findById(userId),
db.posts.findByUser(userId),
db.notifications.findByUser(userId),
])
// ...
}
The Promise.all awaits all three in parallel, and the total time is the time of the slowest request, not the sum of the three . Without it, the second request would wait for the first to finish, which is the sequential waterfall.
c. The promise-passing pattern for the Client Components
A Client Component cannot be async, but it can receive a promise from a Server Component and resolve it with use().
// Server Component
import { Suspense } from 'react'
import { Comments } from './comments'
export default async function PostPage({ id }: { id: string }) {
const post = await getPost(id)
const commentsPromise = getComments(id) // Start, do not await
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
)
}
The commentsPromise is started but not awaited. The post data resolves first, and the Comments Client Component receives the promise as a prop.
// Client Component
'use client'
import { use } from 'react'
export function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
)
}
The use() call suspends the Comments component until the promise resolves . The Suspense boundary in the Server Component shows the fallback in the meantime. The promise was created on the server, so it is stable across the client re-renders; a promise created in the Client Component would be recreated on every render .
The React documentation states the rule explicitly: “Prefer creating Promises in Server Components and passing them to Client Components over creating Promises in Client Components. Promises created in Client Components are recreated on every render” .
Complete Example Session
// ============================================
// PART 1: THE ASYNC SERVER COMPONENT
// ============================================
export default async function PostsPage() {
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
// ============================================
// PART 2: THE SUSPENSE BOUNDARY
// ============================================
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostsPage />
</Suspense>
</div>
)
}
// ============================================
// PART 3: THE PARALLEL FETCHING
// ============================================
async function Dashboard() {
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
])
return <DashboardView user={user} posts={posts} />
}
// ============================================
// PART 4: THE PROMISE PASSING
// ============================================
export default async function PostPage({ id }: { id: string }) {
const post = await getPost(id)
const commentsPromise = getComments(id)
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<p>Loading...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
)
}
// ============================================
// PART 5: THE CLIENT COMPONENT WITH use()
// ============================================
'use client'
import { use } from 'react'
export function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>
}
// ============================================
// PART 6: THE CLIENT ASYNC ERROR
// ============================================
// A Client Component cannot be async.
// React throws: "An unknown Component is an async Client Component."
// Use use() instead.
The six parts demonstrate the async component model: the async Server Component, the Suspense boundary, the parallel fetching, the promise passing, the Client Component with use(), and the client async error.
Quick Reference
Async Components
| Property | Value |
|---|---|
| Valid on server | Yes |
| Valid on client | No (React throws) |
| Syntax | async function Component() { await ... } |
| Suspense | Required for streaming |
| Parallel fetching | Promise.all |
The use() API
| Property | Value |
|---|---|
| Purpose | Read a promise in a Client Component |
| Import | import { use } from 'react' |
| Suspense | Integrates with the boundary |
| Promise source | Prefer from a Server Component |
The Streaming Pattern
| Pattern | Purpose |
|---|---|
<Suspense fallback={...}> | Show the fallback while loading |
loading.tsx | The route-level fallback |
Promise.all | Parallel data fetching |
| Promise passing | Share a server-started promise |
Best Practices
✅ Do This:
// Use the async Server Component for the data access
export default async function Page() { ... } # ✅
// Wrap the async component in Suspense
<Suspense fallback={<Skeleton />}><AsyncComp /></Suspense> # ✅
// Use the Promise.all for the parallel data
await Promise.all([getUser(), getPosts()]) # ✅
// Pass the promise from the server to the client
const promise = getComments(id) # ✅
// Use the use() in the Client Component
const data = use(promise) # ✅
❌ Don’t Do This:
// Make a Client Component async
'use client'
async function Component() { ... } // React throws # ❌
// Use the sequential await for the independent data
const user = await getUser()
const posts = await getPosts() // Waterfall # ❌
// Create the promise in the Client Component
'use client'
const promise = fetchData() // Recreated on every render # ❌
// Forget the Suspense boundary
<AsyncComponent /> // The whole page waits # ❌
// Use the 'use server' for the Server Component
'use server' // This is for Server Functions # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The client async error | async in a Client Component | Use use() or move to the server |
| The page waits for all data | No Suspense boundary | Wrap in <Suspense> |
| The sequential waterfall | Sequential await | Use Promise.all |
| The promise is recreated | Created in the Client Component | Pass from the server |
The 'use server' confusion | Confused with the Server Component | Use it only for Server Functions |
Real-World Examples
1. Async Server Component
export default async function Page() {
const data = await db.query(...)
return <List data={data} />
}
2. Suspense boundary
<Suspense fallback={<Skeleton />}>
<AsyncComponent />
</Suspense>
3. Parallel fetching
const [user, posts] = await Promise.all([getUser(), getPosts()])
4. Promise passing
const promise = getComments(id)
<Comments commentsPromise={promise} />
5. Client use()
'use client'
import { use } from 'react'
const data = use(promise)
6. The client async error
React error #482: "Only Server Components can be async at the moment."
7. The loading.tsx
// app/posts/loading.tsx
export default function Loading() { return <Skeleton /> }
8. The nested Suspense
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
9. The promise in the context
// Pass the promise through context for multiple Client Components
10. The use() error boundary
// Wrap the component in an Error Boundary for the rejected promises
Visual
The Async Component Flow
┌──────────────────────────────────────────────────────────────┐
│ THE ASYNC COMPONENT SUSPENDS UNTIL THE PROMISE RESOLVES │
│ │
│ <Suspense fallback={<Skeleton />}> │
│ <AsyncComponent /> │
│ </Suspense> │
│ │ │
│ ▼ │
│ AsyncComponent awaits the data │
│ │ │
│ ├── pending → the Skeleton is shown │
│ │ │
│ └── resolved → the content is streamed │
└──────────────────────────────────────────────────────────────┘
The Client use() Pattern
┌──────────────────────────────────────────────────────────────┐
│ THE CLIENT COMPONENT CANNOT BE ASYNC, BUT IT CAN USE use() │
│ │
│ Server Component: │
│ ├── const promise = getComments(id) ← start, do not await │
│ └── <Comments commentsPromise={promise} /> │
│ │
│ Client Component: │
│ ├── 'use client' │
│ ├── const data = use(promise) ← read the promise │
│ └── return <List data={data} /> │
│ │
│ The promise was created on the server, so it is stable. │
└──────────────────────────────────────────────────────────────┘
The Parallel vs Sequential
┌──────────────────────────────────────────────────────────────┐
│ Promise.all AVOIDS THE WATERFALL │
│ │
│ ❌ Sequential: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ const user = await getUser() ← 100ms │ │
│ │ const posts = await getPosts() ← 100ms │ │
│ │ Total: 200ms │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ✅ Parallel: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ const [user, posts] = await Promise.all([ │ │
│ │ getUser(), ← 100ms │ │
│ │ getPosts(), ← 100ms │ │
│ │ ]) │ │
│ │ Total: 100ms │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Async components | Server Components only |
| Client async | Not supported (React throws) |
| Client alternative | use() API |
| Suspense | Required for streaming |
| Parallel fetching | Promise.all |
| Promise passing | From the server to the client |
use() import | import { use } from 'react' |
The 'use server' | For Server Functions, not components |
Key takeaways:
- Async components are Server Components only. The
asynckeyword is valid in a Server Component, and it allows the component toawaitthe data in the render body . A Client Component cannot be async; React throws a runtime error . - The
awaitsuspends the component and the Suspense boundary shows the fallback. The async component pauses until the promise resolves, and the Suspense boundary above it streams the fallback to the browser. When the data resolves, the content streams in . - The
Promise.allavoids the sequential waterfall. The independent data fetches should start at the same time, and thePromise.allawaits them in parallel . The sequentialawaitcalls make the total time the sum of the requests, not the maximum. - The Client Component reads the promise with
use(). Theuse()hook unwraps the promise and integrates with Suspense . The promise should be created in the Server Component and passed as a prop, because a promise created in the Client Component is recreated on every render . - The Suspense boundaries should be placed strategically. The static shell (the header, the sidebar, the footer) should render immediately, and the Suspense boundaries should wrap only the dynamic sections . The
loading.tsxis the route-level fallback, and the inline<Suspense>is the granular fallback. - The
'use server'is not for the Server Components. The'use server'directive marks the Server Functions, not the components. There is no directive for the Server Components; the absence of'use client'means the component is a Server Component .
Remember: Async components are the Server Components that await the data in the render. They are valid only on the server, and the Suspense boundary is the mechanism that streams the result. The Promise.all is the pattern for the parallel data, and the use() is the Client Component’s alternative for reading a server-started promise. The result is the streaming rendering where the static shell appears immediately and the dynamic sections arrive as their data resolves, without the client-side waterfall.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!