A powerful abstraction for coordinating multiple promises in React Server Components, eliminating waterfalls and simplifying complex data dependencies.
In modern React applications, managing asynchronous data fetching is a common challenge. While React 18 introduced Suspense for data fetching, coordinating multiple async operations can still be complex. This is where the Await
component comes in - a powerful abstraction that simplifies working with multiple promises in React.
The Await
component is a custom React component that allows you to:
Here's the core implementation:
import type { JSX } from "react";
type Awaited<T> = T extends Promise<infer U> ? U : never;
interface AwaitProps<T extends Promise<any>[]> {
promises: [...T];
children: (...values: { [K in keyof T]: Awaited<T[K]> }) => JSX.Element;
}
export default async function Await<T extends Promise<any>[]>({
promises,
children,
}: AwaitProps<T>) {
const data = await Promise.all(promises);
return children(...(data as any));
}
This component is simple yet powerful. It takes an array of promises and a render function, awaits all the promises, and then calls the render function with the resolved values.
Let's break down how this component works:
Awaited<T>
type extracts the resolved type from a Promise.T extends Promise<any>[]
allows us to work with an array of promises of any type.Promise.all(promises)
waits for all promises to resolve.children
prop is a function that receives the resolved values and returns JSX.Here's a simple example of how to use the Await
component:
import { Suspense } from "react";
import Await from "@/components/await";
export default function UserProfile({ userId }) {
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
return (
<Suspense fallback={<Loading />}>
<Await promises={[userPromise, postsPromise]}>
{(user, posts) => (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
<h2>Posts ({posts.length})</h2>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)}
</Await>
</Suspense>
);
}
The most common use case is fetching data from multiple endpoints:
<Await promises={[fetchUser(userId), fetchPosts(userId), fetchComments(userId)]}>
{(user, posts, comments) => (
<UserDashboard user={user} posts={posts} comments={comments} />
)}
</Await>
You can conditionally include promises:
const promises = [fetchUser(userId)];
if (includeActivity) {
promises.push(fetchUserActivity(userId));
}
return (
<Await promises={promises}>
{(user, ...rest) => {
const activity = includeActivity ? rest[0] : null;
return <UserProfile user={user} activity={activity} />;
}}
</Await>
);
You can transform data before rendering:
<Await promises={[fetchPosts(), fetchCategories()]}>
{(posts, categories) => {
// Transform data
const postsWithCategories = posts.map(post => ({
...post,
categoryName: categories.find(c => c.id === post.categoryId)?.name
}));
return <PostList posts={postsWithCategories} />;
}}
</Await>
You can handle errors within the render function:
<Suspense fallback={<Loading />}>
<ErrorBoundary fallback={<ErrorMessage />}>
<Await promises={[riskyFetch()]}>
{(data) => <DataDisplay data={data} />}
</Await>
</ErrorBoundary>
</Suspense>
You can nest Await
components to create complex data dependencies:
<Await promises={[fetchUser(userId)]}>
{(user) => (
<div>
<UserHeader user={user} />
<Await promises={[fetchFriends(user.friendIds)]}>
{(friends) => <FriendsList friends={friends} />}
</Await>
</div>
)}
</Await>
In Next.js, you can combine with Server Actions:
// Server Action
async function submitComment(formData) {
'use server'
const comment = await saveComment(formData);
return comment;
}
// Component
function CommentSection({ postId }) {
const commentsPromise = fetchComments(postId);
return (
<Suspense fallback={<Loading />}>
<Await promises={[commentsPromise]}>
{(comments) => (
<>
<CommentList comments={comments} />
<CommentForm action={submitComment} />
</>
)}
</Await>
</Suspense>
);
}
async function Await<T extends Promise<any>[]>({
promises,
children,
onError,
}: AwaitProps<T> & { onError?: (error: Error) => JSX.Element }) {
try {
const data = await Promise.all(promises);
return children(...(data as any));
} catch (error) {
if (onError) {
return onError(error as Error);
}
throw error;
}
}
async function AwaitWithTimeout<T extends Promise<any>[]>({
promises,
children,
timeout = 5000,
onTimeout,
}: AwaitProps<T> & {
timeout?: number;
onTimeout?: () => JSX.Element;
}) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeout);
});
try {
const data = await Promise.race([
Promise.all(promises),
timeoutPromise
]);
return children(...(data as any));
} catch (error) {
if (error.message === 'Timeout' && onTimeout) {
return onTimeout();
}
throw error;
}
}
For client components, you might want to track loading progress:
'use client'
import { useState, useEffect } from 'react';
function AwaitWithProgress({ promises, children, fallback }) {
const [results, setResults] = useState(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
let completed = 0;
const total = promises.length;
const wrappedPromises = promises.map(promise =>
promise.then(result => {
completed++;
setProgress(completed / total);
return result;
})
);
Promise.all(wrappedPromises)
.then(data => setResults(data))
.catch(error => console.error(error));
}, [promises]);
if (!results) {
return fallback(progress);
}
return children(...results);
}
// ❌ Creates waterfall requests
async function SequentialComponent() {
const user = await fetchUser();
const posts = await fetchPosts(user.id); // Waits for user first
const comments = await fetchComments(posts.map(p => p.id)); // Waits for posts first
return <Dashboard user={user} posts={posts} comments={comments} />;
}
// ✅ Better, but less reusable
async function ParallelComponent() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return <Dashboard user={user} posts={posts} comments={comments} />;
}
// ✅ Good for simple cases
function Component() {
const user = use(fetchUser());
const posts = use(fetchPosts());
return <Dashboard user={user} posts={posts} />;
}
Await
.Await
component with a Suspense boundary to show loading states.Await
components, it's often cleaner to fetch all data at once.Await
component fetches data in parallel, which is more efficient than sequential fetching.The Await
component is a powerful tool for handling multiple asynchronous operations in React Server Components. It provides a clean, declarative way to coordinate promises and integrate with React's Suspense mechanism.
By using this pattern, you can write more maintainable code that efficiently fetches data in parallel while providing a great user experience with appropriate loading states.
Whether you're building a simple blog or a complex dashboard, the Await
component can help you manage your asynchronous data dependencies with ease.