Suspense
react-md
provides a few Suspense
component wrappers for default fallback
content.
Circular Progress Suspense
The CircularProgressSuspense
component can be used to render a
CircularProgress as the suspense
fallback value that allows all props from the CircularProgress
component and
defaults the aria-label
to Loading
.
This example is a small fork of the
React Suspense Artists example
using react-md
components. Check out the codesandbox for additional notes
around the suspense implementation.
"use client";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { CircularProgressSuspense } from "@react-md/core/suspense/CircularProgressSuspense";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement, use } from "react";
// Note: This is a copy of the suspense codesandbox provided by react:
// https://codesandbox.io/p/sandbox/restless-waterfall-7hzg5z
// Check out the codesandbox for more information around suspense
export default function CircularProgressSuspenseExample(): ReactElement {
const { toggled, enable } = useToggle();
if (toggled) {
return <ArtistPage artist={{ id: "the-beatles", name: "The Beatles" }} />;
}
return <Button onClick={enable}>Load</Button>;
}
interface Artist {
id: string;
name: string;
}
interface ArtistPageProps {
artist: Artist;
}
interface AlbumsProps {
artistId: string;
}
interface ArtistAlbum {
id: number;
title: string;
year: number;
}
type ArtistAlbums = readonly ArtistAlbum[];
function ArtistPage(props: ArtistPageProps): ReactElement {
const { artist } = props;
const { id, name } = artist;
return (
<Box stacked disablePadding align="start">
<Typography type="headline-4" margin="none">
{name}
</Typography>
<CircularProgressSuspense>
<Albums artistId={id} />
</CircularProgressSuspense>
</Box>
);
}
function Albums(props: AlbumsProps): ReactElement {
const { artistId } = props;
const albums = use(fetchData(`/${artistId}/albums`));
return (
<Typography type="subtitle-1" as="ul" margin="none">
{albums.map((album) => {
const { id, title, year } = album;
return (
<li key={id}>
{title} ({year})
</li>
);
})}
</Typography>
);
}
const cache = new Map<string, Promise<ArtistAlbums>>();
async function getAlbums(): Promise<ArtistAlbums> {
// Add a fake delay to make waiting noticeable.
await wait(3000);
return [
{
id: 13,
title: "Let It Be",
year: 1970,
},
{
id: 12,
title: "Abbey Road",
year: 1969,
},
{
id: 11,
title: "Yellow Submarine",
year: 1969,
},
{
id: 10,
title: "The Beatles",
year: 1968,
},
{
id: 9,
title: "Magical Mystery Tour",
year: 1967,
},
{
id: 8,
title: "Sgt. Pepper's Lonely Hearts Club Band",
year: 1967,
},
{
id: 7,
title: "Revolver",
year: 1966,
},
{
id: 6,
title: "Rubber Soul",
year: 1965,
},
{
id: 5,
title: "Help!",
year: 1965,
},
{
id: 4,
title: "Beatles For Sale",
year: 1964,
},
{
id: 3,
title: "A Hard Day's Night",
year: 1964,
},
{
id: 2,
title: "With The Beatles",
year: 1963,
},
{
id: 1,
title: "Please Please Me",
year: 1963,
},
];
}
async function getData(url: string): Promise<ArtistAlbums> {
if (url === "/the-beatles/albums") {
return getAlbums();
}
throw new Error("Not implemented");
}
async function fetchData(url: string): Promise<ArtistAlbums> {
let found = cache.get(url);
if (!found) {
found = getData(url);
cache.set(url, found);
}
return found;
}
Null Suspense
The NullSuspense
component can be used when no fallback value is required such
as:
- lazy loading a small component
- lazy loading the Snackbar on the client
Example
"use client";
import { Button } from "@react-md/core/button/Button";
import { Card } from "@react-md/core/card/Card";
import { CardContent } from "@react-md/core/card/CardContent";
import { CardFooter } from "@react-md/core/card/CardFooter";
import { CardHeader } from "@react-md/core/card/CardHeader";
import { CardTitle } from "@react-md/core/card/CardTitle";
import { NullSuspense } from "@react-md/core/suspense/NullSuspense";
import { useToggle } from "@react-md/core/useToggle";
import { wait } from "@react-md/core/utils/wait";
import {
type FC,
type LazyExoticComponent,
type ReactElement,
lazy,
useMemo,
} from "react";
export default function NullSuspenseExample(): ReactElement {
const { toggled, toggle } = useToggle();
const LazyButton = useFakeLazyImport(Button);
return (
<Card>
<CardHeader>
<CardTitle>Example</CardTitle>
</CardHeader>
<CardContent>
<Button>Hello</Button>
{toggled && (
<NullSuspense>
<LazyButton>World!</LazyButton>
</NullSuspense>
)}
</CardContent>
<CardFooter>
<Button onClick={toggle}>Toggle lazy button</Button>
</CardFooter>
</Card>
);
}
async function fakeImport<P>(
Component: FC<P>,
delay: number,
): Promise<{ default: FC<P> }> {
await wait(delay);
return { default: Component };
}
/**
* This is a hook that will allow lazily import a component each time the `Component`
* changes or the `key` changes so that it can work with `Suspense` from React.
*
* You should probably never do this... but this is a way to make it so that
* the lazy loaded component can be re-loaded infinitely after resetting the
* demo. Without this, the lazy implementation will immediately resolve the
* fake import and not show any progress
*/
export function useFakeLazyImport<P = Record<string, unknown>>(
Component: FC<P>,
delay = 200,
): LazyExoticComponent<FC<P>> {
return useMemo(
() => lazy(() => fakeImport(Component, delay)),
[Component, delay],
);
}