Improve React Loading Experience with Suspense
When building applications with React, have you ever encountered the following situation? A page needs to load a large amount of data, or an action takes quite a while to complete. The interface freezes for a few seconds, and then—bam!—everything updates all at once. This kind of laggy experience feels very jarring and unresponsive. So, is there a way to let the rest of the page render while we wait for time-consuming operations? And only show the delayed content when it's ready?
Let me give you a classic (and cute) example: hatching Pokémon eggs.
First, we need to fetch the list of Pokémon that are about to hatch:
const list = await fetchPokemonList();
The await
blocks further execution until the data is returned. Which means the entire component waits and renders nothing. The user just sees a blank screen. After 2 seconds, the list suddenly appears. Some impatient users may have already assumed the page is broken and started hitting the refresh button.
This is where <Suspense>
comes in.
Step 1: Basic Suspense — Hatch the List
We wrap the list component in a Suspense
boundary and provide a fallback spinner while waiting:
export async function Pokemon() {
const list = fetchPokemonList();
return (
<div>
<p>Try your luck! Which Pokémon can you hatch from the Eggs?</p>
<Suspense fallback={<LoadingListSpinner />}>
<List promise={list} />
</Suspense>
</div>
);
}
We put ‘await’ in List component, it waits for the data and then renders the Pokémon:
async function List(props: {
promise: Promise<Pokemon[]>;
}) {
const list = await props.promise;
return (
<div>
{list.map(async (pokemon) => (<PokemonCard pokemon={pokemon}/>))}
</div>
);
}
While the Pokemon List is still loading, it will suspend and React will render the fallback component instead. Meanwhile, other parts of the page stay visible.
)
Much better, right? Now users can tell: “Ah, they’re hatching! I’m about to meet my Pokémon!”
Step 2: Suspense Per Item — Let the Fast Ones Hatch First
But there’s still one more issue.
What if one single Pokémon is slow to load? It becomes the bottleneck, blocking the entire list. No Pokémon is shown until the slowest one arrives. Ouch.
What can we do? Yes, <Suspense>
can be nested! Let’s improve that by wrapping each Pokemon Item with its own Suspense. The faster Pokémon can appear first, while the slower ones still show a spinner:
async function List(props: {
promise: Promise<Pokemon[]>;
}) {
const list = await props.promise;
return (
<div>
{list.map((pokemon) => (
<div>
<p>{pokemon.name}</p>
<Suspense fallback={<Spinner />}>
<CardContent promise={fetchPokemonImage(pokemon)} />
</Suspense>
</div>
))}
</div>
);
}
async function CardContent(props: { promise: Promise<Pokemon> }) {
const pokemon = await props.promise;
return ( <Image src={pokemon.image} /> );
}
Now it looks even smoother. Users start seeing Pokémon as soon as they’re ready, one by one. In fact, they might not even notice that some are still loading, when they focus on these already there.
)
Bonus: Use useTransition
for Smoother Updates
Suspense
works excellently. After clicking, the page starts loading, but the user interface does not update immediately, so the user perceives a slight delay.
)
In such cases, we can pair it with startTransition or useTransition to improve the visual smoothness.
const [isPending, startTransition] = useTransition();
const onClick = () => {
startTransition(() => {
navigateToNewPage();
});
};
This hook allows React to keep the old UI visible while rendering the new UI in the background. During this process, it exposes a pending state that we can provide better feedback and smoother user experience.
)
Conclusion
React Suspense is a powerful tool that can dramatically improve perceived performance:
- Show fallbacks while loading slow parts of the page
- Render faster parts first with nested Suspense
- Make a better user experience with useTransition
Once you try it, your app will feel much smoother and more responsive. So go ahead—start hatching some Suspense-powered Pokémon today!
)
)
)
)
)
)
)
)
)
)
)
)