Introduction to React Suspense
Published: 2022-12-28 06:01:00

## Introduction to Suspense

React `<Suspense>` is a Wrapper component used to Show a fallback Components until the child component completes the operations like `fetch()` or any other asynchronous Operations. It's useful when we need to show a fallback component `I.e., Loading...` while one of its child components fails to render or takes a long time to complete the operations.

> React Suspense feature is released along with React version 16.8.

## Setup the React Project

In this project I use my favorite bundling tool called [vite](https://vitejs.dev/), It is a fast compiler & bundler, and it uses [rollup](https://rollupjs.org/) under the hood.

### To create a new React project

```
yarn create vite react-suspense --template react-ts
```

Now open the `react-suspense` directory on your editor, then do `yarn install` to install the necessary packages. To Style the components, I use my favorite tool called [Tailwind-CSS](https://tailwindcss.com/). To install the Tailwind-CSS refer the [Documentation](https://tailwindcss.com/docs/installation) before moving to further steps.

We are going to build the SPA React app with `Suspense`. For the backend API,  we are going to use the [RickandMorty](https://rickandmortyapi.com/) API.

## Preview of our app
![RickandMorty](https://media.giphy.com/media/ovIKzqSY0OXFnqLrER/giphy.gif)


## Setup
 
To set up `Suspense` open `App.tsx` and copy-paste the below code Snippet. here I have additionally set up the Routes and [lazily](https://reactjs.org/docs/code-splitting.html) imported our pages to work with Suspense.

`src/App.tsx`

```typescript
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Spinner from './Components/Spinner';

const Character = lazy(() => import('./Pages/Character'));
const Home = lazy(() => import('./Pages/Home'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/character/:id" element={<Character />} />
        </Routes>
      </BrowserRouter>
    </Suspense>
  );
}

export default App;

```
Create one more component in `Components/Spinner.tsx`, This Spinner Component will be used as the fallback component on our Suspense Component.

`src/Components/Spinner.tsx`

```typescript
import React from 'react';

function Spinner() {
  return (
    <div className="h-screen flex justify-center items-center">
      <div
        className="spinner-border animate-spin block w-14 h-14 border-4 border-t-amber-600 rounded-full"
        role="status"
      />
    </div>
  );
}

export default Spinner;
```

Now we are done with our basic Setup for Suspense. Next, we are going to look at Data fetching with fetch.

## Data fetching using Suspense and Fetch

Create the `Home.tsx` and `Character.tsx` under the `pages` directory and copy-paste the below code Snippet.

`src/Pages/Home.tsx`

```typescript
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Pagination } from '../Components/Pagination';

function Home() {
  const [data, setData] = useState<any>({});

  const fetchCharacters = async (page?: number) => {
    const response = await fetch(
      `https://rickandmortyapi.com/api/character?page=${page}`
    );

    const parsedData = await response.json();

    if (response.ok) setData(parsedData);
  };

  useEffect(() => {
    fetchCharacters(1);
  }, []);

  const { results, info } = data;

  const onPageChange = (pageNumber: number) => {
    fetchCharacters(pageNumber);
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  };

  return (
        <div className="xl:container px-4 md:px-8 lg:px-28 mx-auto">
      <h1 className="text-blue-500 py-10 text-4xl text-center font-bold">
        Rick And Morty
      </h1>

      <ul className="list-none grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 w-full">
        {results?.map((datas: any) => {
          const {
            id,
            name,
            species,
            gender,
            origin,
            location,
            image,
            episode,
          } = datas;
          return (
            <li
              key={id}
              className="p-4 border rounded-xl shadow-md hover:shadow-xl"
            >
              <Link
                state={{
                  episode: JSON.stringify(episode),
                  user: {
                    name,
                    species,
                    image,
                    origin,
                    location,
                  },
                }}
                to={`character/${id}`}
              >
                <img
                  alt={name}
                  src={image}
                  className="rounded-lg object-cover w-full h-auto"
                />
                <div className="mt-5 flex gap-2">
                  <div>
                    {name ? <p>Name: </p> : null}
                    {species ? <p>Species: </p> : null}
                    {gender ? <p>Gender: </p> : null}
                    {origin.name ? <p>Origin: </p> : null}
                    {location.name ? <p>Location: </p> : null}
                  </div>
                  <div>
                    {name ? <h4> {name}</h4> : null}
                    {species ? <p> {species}</p> : null}
                    {gender ? <p>{gender}</p> : null}
                    {origin.name ? <p>{origin.name}</p> : null}
                    {location.name ? <p> {location.name}</p> : null}
                  </div>
                </div>
              </Link>
            </li>
          );
        })}
      </ul>
      {info ? (
        <Pagination
          pageCount={info.count}
          className="py-10 w-full"
          pageSize={20}
          onPageChange={onPageChange}
        />
      ) : null}
    </div>
  );
}

export default Home;
```

On the Home page, we are fetching the characters & showing the list on UI with few character details.

> I have used my Pagination component from `Components/Pagination.tsx`, You can refer to the code on my [Github](https://github.com/gokul1630/rickandmorty/blob/main/src/Components/Pagination.tsx)

`src/Pages/Character.tsx`

```typescript
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';

function Character() {
  const [episodeData, setEpisodeData] = useState<any[]>([]);
  const [locationsData, setLocationsData] = useState({
    name: '',
    dimension: '',
    residents: [],
  });

  const fetchEpisodes = async (episode: string) => {
    const response = await fetch(episode);
    const data = await response.json();
    setEpisodeData((state) => [...state, data.name]);
  };

  const fetchLocations = async (episode: string) => {
    const response = await fetch(episode);
    const data = await response.json();

    setLocationsData(data);
  };

  const { state } = useLocation() as any;

  const { name, species, origin, image, location } = state.user;

  useEffect(() => {
    // fetch episodes
    const data = JSON.parse(state.episode) as unknown as Array<any>;
    data.map(fetchEpisodes);

    // fetch locations
    fetchLocations(location.url);
  }, []);

  return (
    <div className="flex justify-center flex-wrap gap-10 p-4 md:p-8">
      <img
        alt={name}
        src={image}
        className="rounded-md object-cover self-start w-full md:max-w-md h-auto shadow-md hover:shadow-xl"
      />
      <div>
        <div>
          {name ? <h4 className="text-xl">{`Name: ${name}`}</h4> : null}
          {species ? (
            <p className="text-xl mt-2">{`Species: ${species}`}</p>
          ) : null}
          {origin.name ? (
            <p className="text-xl mt-2">{`Origin: ${origin.name}`}</p>
          ) : null}
          {locationsData.name ? (
            <p className="text-xl mt-2">{`Dimension: ${locationsData.dimension}`}</p>
          ) : null}
          {locationsData.name ? (
            <p className="text-xl mt-2">{`Location: ${locationsData.name}`}</p>
          ) : null}
          {locationsData.residents ? (
            <p className="text-xl mt-2">{`Amount of Residents: ${locationsData.residents.length}`}</p>
          ) : null}
        </div>
        <h1 className="text-2xl lg:text-3xl text-blue-500 mt-4">
          Episodes Appeared
        </h1>
        <ol className="mt-4">
          {episodeData?.map((episode, idx) => (
            <li key={episode} className="text-lg lg:text-xl text-gray-800">
              {idx + 1}. {episode}
            </li>
          ))}
        </ol>
      </div>
    </div>
  );
}

export default Character;
```

On the Character page, we are fetching the Specific Character Episodes and Locations from API based on the data we are passed from HomePage using the Router state feature.

Now we are done with our basic react app setup with suspense. We can preview the Character's list page and Character view page.

![throttled network request](https://media.giphy.com/media/XArshTUhrFxWUOaCiu/giphy.gif)
> I have throttled the network request to `Fast 3G` to preview the fallback component before our actual component renders.

## Suspense with useTransition()

[useTransition()](https://reactjs.org/docs/hooks-reference.html#usetransition) is a new Hook introduced with React 18. This hook allows the developer to make use of  Concurrent rendering to  provide a better user experience in their Applications. To use this hook, We have to update the Home.tsx & Character.tsx like below code snippets. 

`src/Pages/Home.tsx`

```typescript
import React, { useEffect, useState, useTransition } from 'react';
import { Link } from 'react-router-dom';
import Loader from '../Components/Loader';
import { Pagination } from '../Components/Pagination';

function Home() {
  const [data, setData] = useState<any>({});
  const [isLoading, startTransition] = useTransition();

  const fetchCharacters = async (page?: number) => {
    const response = await fetch(
      `https://rickandmortyapi.com/api/character?page=${page}`
    );

    const parsedData = await response.json();
    startTransition(() => {
      if (response.ok) setData(parsedData);
    });
  };

  useEffect(() => {
    fetchCharacters(1);
  }, []);

  const { results, info } = data;

  const onPageChange = (pageNumber: number) => {
    fetchCharacters(pageNumber);
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  };

  return (
    <div className="xl:container px-4 md:px-8 lg:px-28 mx-auto">
      <h1 className="text-blue-500 py-10 text-4xl text-center font-bold">
        Rick And Morty
      </h1>

      {!isLoading ? (
        <>
          <ul className="list-none grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 w-full">
            {results?.map((datas: any) => {
              const {
                id,
                name,
                species,
                gender,
                origin,
                location,
                image,
                episode,
              } = datas;
              return (
                <li
                  key={id}
                  className="p-4 border rounded-xl shadow-md hover:shadow-xl"
                >
                  <Link
                    state={{
                      episode: JSON.stringify(episode),
                      user: {
                        name,
                        species,
                        image,
                        origin,
                        location,
                      },
                    }}
                    to={`character/${id}`}
                  >
                    <img
                      alt={name}
                      src={image}
                      className="rounded-lg object-cover w-full h-auto"
                    />
                    <div className="mt-5 flex gap-2">
                      <div>
                        {name ? <p>Name: </p> : null}
                        {species ? <p>Species: </p> : null}
                        {gender ? <p>Gender: </p> : null}
                        {origin.name ? <p>Origin: </p> : null}
                        {location.name ? <p>Location: </p> : null}
                      </div>
                      <div>
                        {name ? <h4> {name}</h4> : null}
                        {species ? <p> {species}</p> : null}
                        {gender ? <p>{gender}</p> : null}
                        {origin.name ? <p>{origin.name}</p> : null}
                        {location.name ? <p> {location.name}</p> : null}
                      </div>
                    </div>
                  </Link>
                </li>
              );
            })}
          </ul>
          {info ? (
            <Pagination
              pageCount={info.count}
              className="py-10 w-full"
              pageSize={20}
              onPageChange={onPageChange}
            />
          ) : null}
        </>
      ) : (
        <Loader />
      )}
    </div>
  );
}

export default Home;

```

`src/Pages/Character.tsx`

```typescript
import React, { useEffect, useState, useTransition } from 'react';
import { useLocation } from 'react-router-dom';
import Loader from '../Components/Loader';

function Character() {
  const [episodeData, setEpisodeData] = useState<any[]>([]);
  const [locationsData, setLocationsData] = useState({
    name: '',
    dimension: '',
    residents: [],
  });
  const [isLoading, startTransition] = useTransition();

  const fetchEpisodes = async (episode: string) => {
    const response = await fetch(episode);
    const data = await response.json();
    startTransition(() => {
      setEpisodeData((state) => [...state, data.name]);
    });
  };

  const fetchLocations = async (episode: string) => {
    const response = await fetch(episode);
    const data = await response.json();
    startTransition(() => {
      setLocationsData(data);
    });
  };

  const { state } = useLocation() as any;

  const { name, species, origin, image, location } = state.user;

  useEffect(() => {
    // fetch episodes
    const data = JSON.parse(state.episode) as unknown as Array<any>;
    data.map(fetchEpisodes);

    // fetch locations
    fetchLocations(location.url);
  }, []);

  return (
    <div className="flex justify-center flex-wrap gap-10 p-4 md:p-8">
      <img
        alt={name}
        src={image}
        className="rounded-md object-cover self-start w-full md:max-w-md h-auto shadow-md hover:shadow-xl"
      />
      {!isLoading ? (
        <div>
          <div>
            {name ? <h4 className="text-xl">{`Name: ${name}`}</h4> : null}
            {species ? (
              <p className="text-xl mt-2">{`Species: ${species}`}</p>
            ) : null}
            {origin.name ? (
              <p className="text-xl mt-2">{`Origin: ${origin.name}`}</p>
            ) : null}
            {locationsData.name ? (
              <p className="text-xl mt-2">{`Dimension: ${locationsData.dimension}`}</p>
            ) : null}
            {locationsData.name ? (
              <p className="text-xl mt-2">{`Location: ${locationsData.name}`}</p>
            ) : null}
            {locationsData.residents ? (
              <p className="text-xl mt-2">{`Amount of Residents: ${locationsData.residents.length}`}</p>
            ) : null}
          </div>
          <h1 className="text-2xl lg:text-3xl text-blue-500 mt-4">
            Episodes Appeared
          </h1>
          <ol className="mt-4">
            {episodeData?.map((episode, idx) => (
              <li key={episode} className="text-lg lg:text-xl text-gray-800">
                {idx + 1}. {episode}
              </li>
            ))}
          </ol>
        </div>
      ) : (
        <Loader />
      )}
    </div>
  );
}

export default Character;
```

![suspense with useTransition()](https://media.giphy.com/media/gopv3xtTwv0HJkf05A/giphy.gif)

After we made the changes on both pages, we can see the spinner appearing on UI for a few milliseconds.


## Data fetching using react-query

Now we are going to migrate our project to use the [react-query](https://react-query-v3.tanstack.com/), Accroding to the Documentation Overview `React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.` you can read more about it [here](https://react-query-v3.tanstack.com/overview#motivation).

First, we should wrap our components with [QueryClientProvider](https://react-query-v3.tanstack.com/reference/QueryClientProvider#_top) to the top level like below code snippet.

`src/App.tsx`

```typescript
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Spinner from './Components/Spinner';

const Character = React.lazy(() => import('./Pages/Character'));
const Home = React.lazy(() => import('./Pages/Home'));

function App() {
  const client = new QueryClient({
    defaultOptions: {
      queries: {
        suspense: true,
      },
    },
  });

  return (
    <QueryClientProvider client={client}>
      <React.Suspense fallback={<Spinner />}>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/character/:id" element={<Character />} />
          </Routes>
        </BrowserRouter>
      </React.Suspense>
    </QueryClientProvider>
  );
}

export default App;
```

Now, we have to write our API queries in a separate file, So create a file in `queries/queries.ts`.

`src/queries/queries.ts`

```typescript
export async function fetchCharacters(pageNumber?: string | number) {
  const response = await fetch(
    `https://rickandmortyapi.com/api/character?page=${pageNumber}`
  );

  return response.json();
}

export async function fetchEpisodes(episode: any) {
  const response = await fetch(episode);
  return response.json();
}

export async function fetchLocations(episode: any) {
  const response = await fetch(episode);
  return response.json();
}
```

We have separated our data-fetching query functions. Now, We have to Update our Home and Character Pages to use the `react-query`. Copy & paste the below code snippets to update the Pages.

`src/Pages/Home.tsx`

```typescript
import React from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { Link } from 'react-router-dom';
import { Loader } from '../Components/Loader';
import { Pagination } from '../Components/Pagination';
import { fetchCharacters } from '../queries/queries';

function Home() {
  const [activePage, setActivePage] = React.useState(1);

  const { prefetchQuery } = useQueryClient();

  const queryObj = {
    queryKey: ['fetchCharacters', activePage],
    queryFn: () => fetchCharacters(activePage),
  };

  const { data, isLoading } = useQuery(queryObj);

  const { results, info } = data as any;

  const onPageChange = (pageNumber: number) => {
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });

    setActivePage(pageNumber);
    prefetchQuery({
      queryKey: 'fetchCharacters',
      queryFn: () => fetchCharacters(activePage),
    });
  };

  return (
    <div className="xl:container px-4 md:px-8 lg:px-28 mx-auto">
      <h1 className="text-blue-500 py-10 text-4xl text-center font-bold">
        Rick And Morty
      </h1>
      <ul className="list-none grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 w-full">
        {results?.map((datas: any) => {
          const {
            id,
            name,
            species,
            gender,
            origin,
            location,
            image,
            episode,
          } = datas;
          return (
            <li
              key={id}
              className="p-4 border rounded-xl shadow-md hover:shadow-xl"
            >
              <Link
                state={{
                  episode: JSON.stringify(episode),
                  user: {
                    name,
                    species,
                    image,
                    origin,
                    location,
                  },
                }}
                to={`character/${id}`}
              >
                <img
                  alt={name}
                  src={image}
                  className="rounded-lg object-cover w-full h-auto"
                />
                {!isLoading ? (
                  <div className="mt-5 flex gap-2">
                    <div>
                      {name ? <p>Name: </p> : null}
                      {species ? <p>Species: </p> : null}
                      {gender ? <p>Gender: </p> : null}
                      {origin.name ? <p>Origin: </p> : null}
                      {location.name ? <p>Location: </p> : null}
                    </div>
                    <div>
                      {name ? <h4> {name}</h4> : null}
                      {species ? <p> {species}</p> : null}
                      {gender ? <p>{gender}</p> : null}
                      {origin.name ? <p>{origin.name}</p> : null}
                      {location.name ? <p> {location.name}</p> : null}
                    </div>
                  </div>
                ) : (
                  <Loader />
                )}
              </Link>
            </li>
          );
        })}
      </ul>
      {info ? (
        <Pagination
          pageCount={info.count}
          className="py-10 w-full"
          pageSize={20}
          onPageChange={onPageChange}
        />
      ) : null}
    </div>
  );
}

export default Home;
```

`src/Pages/Character.tsx`

```typescript
import React from 'react';
import { useQueries, useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Loader } from '../Components/Loader';
import { fetchEpisodes, fetchLocations } from '../queries/queries';

function Character() {
  const { state } = useLocation() as any;

  const { name, species, origin, image, location } = state.user;

  const { data, isLoading } = useQuery({
    queryKey: 'fetchLocations',
    queryFn: () => fetchLocations(location.url),
  });

  const { name: locationName, dimension, residents } = data as any;

  const episodeUrls = JSON.parse(state.episode) as unknown as Array<any>;

  const queriedData = useQueries([
    ...episodeUrls.map((url) => {
      return {
        queryKey: url,
        queryFn: () => fetchEpisodes(url),
      };
    }),
  ]);

  return (
    <div className="flex justify-center flex-wrap gap-10 p-4 md:p-8">
      <img
        alt={name}
        src={image}
        className="rounded-md object-cover self-start w-full md:max-w-md h-auto shadow-md hover:shadow-xl"
      />
      <div>
        <div>
          {name ? <h4 className="text-xl">{`Name: ${name}`}</h4> : null}
          {species ? (
            <p className="text-xl mt-2">{`Species: ${species}`}</p>
          ) : null}
          {origin.name ? (
            <p className="text-xl mt-2">{`Origin: ${origin.name}`}</p>
          ) : null}
          {!isLoading ? (
            <p className="text-xl mt-2">{`Dimension: ${dimension}`}</p>
          ) : (
            <Loader />
          )}
          {!isLoading ? (
            <p className="text-xl mt-2">{`Location: ${locationName}`}</p>
          ) : (
            <Loader />
          )}
          {!isLoading ? (
            <p className="text-xl mt-2">{`Amount of Residents: ${residents.length}`}</p>
          ) : (
            <Loader />
          )}
        </div>
        <h1 className="text-2xl lg:text-3xl text-blue-500 mt-4">
          Episodes Appeared
        </h1>

        <ol className="mt-4">
          {queriedData?.map(
            ({ isLoading: loading, data: episodeData = {} }, idx) => {
              const { name: episodeName, id } = episodeData;
              return (
                <>
                  {loading ? (
                    <Loader />
                  ) : (
                    <li key={id} className="text-lg lg:text-xl text-gray-800">
                      {idx + 1}. {episodeName}
                    </li>
                  )}
                  {false}
                </>
              );
            }
          )}
        </ol>
      </div>
    </div>
  );
}

export default Character;

```

> I have used the `useQueries()` to fetch the Array of  episodes url's.

Look how simple this is. Now, we can completly avoid using the `useEffect` & `useState` hooks for fetching the data after our Components are mounted to the DOM.

We can view the `Loading...` text in-between the components because I have throttled the network request.
![with network throttle](https://media.giphy.com/media/hGSSUNG2BQw05lTYJ4/giphy.gif)

Without the network throttle, the app loads faster than before.
![without network throttle](https://media.giphy.com/media/LUX7xhiB2jeCku5vae/giphy.gif)


## Conclusion

React Suspense is very useful because it works with only lazy imports, so here we are splitting our components into separate chunks while building our project, It helps to prevent downloading all the Javascript files when a user opens our website for the first time. Also, it helps to show the fallback UI until the child components are ready to show themselves. 

I always encourage everyone to Implement React Suspense into their projects.

## Help me out

I would be happy if this post helps you to understand the React Suspense. Please give a like and Star on GitHub.

https://github.com/gokul1630/rickandmorty

Thankyou for reading!!