When working with Next.js and fetching data in React Server Components, I've found myself creating many small wrapper components just to use Suspense
.
Imagine you have such component that renders a Select
component, which awaits with use
hook it's options from optionsPromise
(let's imagine the promise came from a parent component):
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
const options = use(optionsPromise);
return <Select options={options} />;
};
One issue with this component is that this component would block the rendering of the page until the options promise is resolved. To solve this, we would need to wrap this component in a suspense boundary and ideally pass some fallback to be shown while the Suspense
boundary content is suspended.
const WrapperSelectWithOptions = ({ optionsPromise }: Promise<string[]>) => {
return (
<Suspense fallback={<SelectSkeleton />}>
<SelectWithOptions optionsPromise={optionsPromise} />
</Suspense>
);
};
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
const options = use(optionsPromise);
return <Select options={options} />;
};
As we can see, we needed to create a wrapper component just to be able to wrap the component that uses use
to await the promise. This might not be too painful, but it can be annoying to need to create such wrapper components in each such case.
Solution
To avoid creating such wrapper components, we can define a helper component Use
that would use the use
hook and pass the resolved value as a prop to a children
render function:
const Use = <T,>({
children,
promise,
}: {
children: (value: T) => ReactNode;
promise: Promise<T>;
}) => {
return children(use(promise));
};
and then we would use it like this:
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
return (
<Suspense fallback={<SelectSkeleton />}>
<Use promise={optionsPromise}>
{(options) => <Select options={options} />}
</Use>
</Suspense>
);
};
An extra win we get is that now all the code to render the SelectWithOptions
component is colocated in one place: both the Suspense
with the fallback skeleton component and the Select
component itself with its options.
React Server components
This approach also works with React Server components. We just need to create another helper component Await
:
const Await = async <T,>({
children,
promise,
}: {
children: (value: T) => ReactNode;
promise: Promise<T>;
}) => {
return children(await promise);
};
and then use it like this:
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
return (
<Suspense fallback={<SelectSkeleton />}>
<Await promise={optionsPromise}>
{(options) => <Select options={options} />}
</Await>
</Suspense>
);
};
Error boundary
One extra step could be to add an error boundary around the Suspense
boundary.
For this, we can define our own error boundary class component (since there is no way to use a function component for that), or use a library like react-error-boundary
.
Now, the final code snippet would look like this:
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
return (
<ErrorBoundary fallback={<ErrorComponent />}>
<Suspense fallback={<SelectSkeleton />}>
<Await promise={optionsPromise}>
{(options) => <Select options={options} />}
</Await>
</Suspense>
</ErrorBoundary>
);
};
Looking at code, you might feel an urge to create some more high-level component that would wrap all of this together to avoid the boilerplate.
This, of course, can be done, and it could look something like this:
const SelectWithOptions = (optionsPromise: Promise<string[]>) => {
return (
<SuspenseAwait
loading={<SelectSkeleton />}
error={<ErrorComponent />}
promise={optionsPromise}
>
{(options) => <Select options={options} />}
</SuspenseAwait>
);
};
and the implementation:
const SuspenseAwait = <T,>({
loading,
error,
promise,
children,
}: {
loading?: ReactNode;
error?: ReactNode;
promise: Promise<T>;
children: (value: T) => ReactNode;
}) => {
return (
<ErrorBoundary fallback={error}>
<Suspense fallback={loading}>
<Await promise={promise}>{children}</Await>
</Suspense>
</ErrorBoundary>
);
};
Similar SuspenseUse
could be created for the usage of use
hook in client components.
Summary
I've found this approach useful when working with Next.js and React Server components. Instead of splitting a child component that fetches the data, and the Suspense
and ErrorBoundary
components to stay in the parent component in order to wrap the child component, we can just use component composition and render functions to colocate everything nicely together.
I hope you'll find this approach useful for you as well!