How to Test Loading and Error States With Server Components
Table of Contents
Server components enable you to render the entire pages on the server without messing around with API endpoints and data fetching logic. But handling the different states remains an issue for the optimal usability experience. Without handling these states the user interface feels sluggish while waiting for the server response and an error would render the entire screen useless with a white screen of death. Not ideal, but strategies to mitigate these undesirable behaviors do exist and are easy to use.
#
Adding loading and error state
I am using server components to render a list of AI-generated snippets on AIHelperBot. It has the following folder structure:
--app /
--snippets /
--error.tsx;
--loading.tsx;
--page.tsx;
Both error.tsx
and loading.tsx
export a default React component that is displayed instead of the page.tsx
when an error occurs or when loading. The loading.tsx
looks like this:
import { H1AsH2, Link, P } from '@/components/Typo';
export default function Loading() {
return (
<>
// Breadcrumbs omitted for brevity...
<div className="mx-auto max-w-4xl p-4">
<div className="mb-8">
<H1AsH2>Snippets</H1AsH2>
<P>
Your snippet library. You can create new snippets using the{' '}
<Link href="/app">AI Generator</Link>.
</P>
</div>
<div
role="status"
className="dark:divide-gray-700 animate-pulse space-y-4 divide-y divide-gray-200 rounded"
>
<div className="flex items-center justify-between">
<div>
<div className="dark:bg-gray-600 mb-2.5 h-2.5 w-24 rounded-full bg-gray-300"></div>
<div className="dark:bg-gray-700 h-2 w-32 rounded-full bg-gray-200"></div>
</div>
<div className="dark:bg-gray-700 h-2.5 w-12 rounded-full bg-gray-300"></div>
</div>
// Repeated skeleton pattern omitted for brevity...
<span className="sr-only">Loading...</span>
</div>
</div>
</>
);
}
And the error.tsx
:
'use client';
import { Button } from '@/components/Button';
import { H1AsH2 } from '@/components/Typo';
import { Alert } from '@/components/Alert';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<>
// Breadcrumbs omitted for brevity...
<div className="mx-auto max-w-4xl p-4">
<H1AsH2>Snippets</H1AsH2>
<Alert variant="error">{error.message}</Alert>
<Button color="light" onClick={reset}>
Try again
</Button>
</div>
</>
);
}
Both are simple stateless React components.
#
Visualizing loading and error states
Here are the snippet page and its different states.
##
Server-side rendered snippet page
(Please excuse the local test data)
##
Snippet loading state
It is a pulsating skeleton which displays immediately when navigating to the snippet page.
##
Snippet error state
An error occurred on the server while rendering the page.
Both the loading and error states are easy to work with since they merely contains JSX
and Next.JS handles switching between them. But how to test them? Of course you could manually throw an error or artificially extend the duration of a database call, but those hacks to test the states might accidentally slip into production and cause bugs. Besides it is cumbersome work flow constantly having to add, remove or uncommend testing code.
#
Testing loading and error state
To enable easy testing you can add a helper function that enables you to switch between the states using URL parameters:
/**
* Test loading and error layouts for NextJS's app dir.
* @param searchParams
* @returns Promise
*/
export const layoutTester = async (searchParams: Record<string, string>) => {
if (process.env.NODE_ENV !== 'development') {
return;
}
if (typeof searchParams.loading !== 'undefined') {
const loading = parseInt(searchParams.loading || '2000');
await new Promise((resolve) => setTimeout(resolve, loading));
}
if (typeof searchParams.error !== 'undefined') {
const error = searchParams.error || 'Something went wrong!';
await new Promise((_resolve, reject) => reject(error));
}
};
Use it by adding it to the server component, in this case the app/snippets/page.tsx
:
import { layoutTester } from '@lib/layoutTester';
// Next.JS isn't exporting a type for the server component props unfortunately
export default async function Snippet(props: {
searchParams: Record<string, string>;
}) {
await layoutTester(props.searchParams);
// Do what else you need to like checking credentials
// and connecting to the database
return <div>{
/*Omitted for brevity */
}<div>;
}
To test the different states simple add the parameters to the page URL (e.g. http://localhost:3000/app/snippets?loading
):
- loading state append
?loading
or?loading=10000
(duration in ms) - error state append
?error
or?error="Custom error message."
- loading and error state append
?loading&error
Appending these parameters only works in development mode. It makes it easy to ensure a smooth flow and sane error handling.