DEV Community

Cover image for Introduction to React Suspense
Gokulprasanth
Gokulprasanth

Posted on • Edited on

Introduction to React Suspense

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, It is a fast compiler & bundler, and it uses rollup under the hood.

To create a new React project

yarn create vite react-suspense --template react-ts
Enter fullscreen mode Exit fullscreen mode

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. To install the Tailwind-CSS refer the Documentation 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 API.

Preview of our app

RickandMorty

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 imported our pages to work with Suspense.

src/App.tsx

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;

Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

src/Pages/Character.tsx

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;
Enter fullscreen mode Exit fullscreen mode

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

I have throttled the network request to Fast 3G to preview the fallback component before our actual component renders.

Suspense with useTransition()

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

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;

Enter fullscreen mode Exit fullscreen mode

src/Pages/Character.tsx

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;
Enter fullscreen mode Exit fullscreen mode

suspense with useTransition()

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, 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.

First, we should wrap our components with QueryClientProvider to the top level like below code snippet.

src/App.tsx

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;
Enter fullscreen mode Exit fullscreen mode

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

src/queries/queries.ts

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();
}
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

src/Pages/Character.tsx

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;

Enter fullscreen mode Exit fullscreen mode

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

Without the network throttle, the app loads faster than before.
without network throttle

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!!

Top comments (5)

Collapse
 
fpaghar profile image
Fatemeh Paghar • Edited

Please write the code blog like

Image description
It shows your code with color and It will be easy to read.

Collapse
 
gokul1630 profile image
Gokulprasanth

Thanks I will try

Collapse
 
664848 profile image
DAREALENDERJACK

you know we really need a better guide for stuff like that

Collapse
 
fpaghar profile image
Fatemeh Paghar

github.com/adam-p/markdown-here/wi...
It is a good reference to markup text.

Thread Thread
 
664848 profile image
DAREALENDERJACK

thx