Skip to main content
react-md

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;
}

Press Enter to start editing.

Null Suspense

The NullSuspense component can be used when no fallback value is required such as:

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],
  );
}

Press Enter to start editing.