Lädt...


🔧 Introduction to React Suspense


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

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

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;

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;


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;

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;

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;

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;

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;

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

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;

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;

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

...

🔧 Learn Suspense by Building a Suspense-Enabled Library


📈 47.4 Punkte
🔧 Programmierung

🔧 Introduction to React Suspense


📈 40.29 Punkte
🔧 Programmierung

🔧 How to Use React Suspense to Improve your React Projects


📈 37.86 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [24] Suspense(1) - Render Fallback


📈 30.78 Punkte
🔧 Programmierung

🔧 React Suspense for data fetching


📈 30.78 Punkte
🔧 Programmierung

🔧 Async React with Suspense


📈 30.78 Punkte
🔧 Programmierung

🔧 TLDR; Suspense in react-query


📈 30.78 Punkte
🔧 Programmierung

🔧 Understanding Suspense and Suspended Components in React


📈 30.78 Punkte
🔧 Programmierung

🔧 Exploring React v19: Elevating User Experiences with Concurrent Mode and Suspense


📈 30.78 Punkte
🔧 Programmierung

🔧 React Suspense: Improving the Performance and Usability of Your Application


📈 30.78 Punkte
🔧 Programmierung

🔧 What is React Suspense and Async Rendering?


📈 30.78 Punkte
🔧 Programmierung

🔧 ChatGPT clone with React Suspense and Streaming


📈 30.78 Punkte
🔧 Programmierung

🔧 The Ultimate Guide to React: Conquering Concurrent Mode and Suspense


📈 30.78 Punkte
🔧 Programmierung

🔧 Unveiling the Future of React: A Dive into Concurrent Mode and Suspense


📈 30.78 Punkte
🔧 Programmierung

🔧 Improving Performance with React Lazy and Suspense


📈 30.78 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [25] Suspense(2) - Data Fetching with use hook


📈 30.78 Punkte
🔧 Programmierung

🔧 This Week In React #185: React Conf, React Query, refs, Next.js after, mini-react...


📈 28.32 Punkte
🔧 Programmierung

🔧 This Week In React #185: React Conf, React Query, refs, Next.js after, mini-react...


📈 28.32 Punkte
🔧 Programmierung

🔧 Wait for pending: A (not great) alternative Suspense algorithm


📈 23.7 Punkte
🔧 Programmierung

🔧 How to used Suspense?


📈 23.7 Punkte
🔧 Programmierung

🔧 There was an error while hydrating this Suspense boundary. Switched to client rendering. Next.js 14


📈 23.7 Punkte
🔧 Programmierung

🔧 Loading.... Suspense


📈 23.7 Punkte
🔧 Programmierung

🔧 New Suspense Hooks for Meteor


📈 23.7 Punkte
🔧 Programmierung

🔧 Understanding Suspense with Next 13


📈 23.7 Punkte
🔧 Programmierung

📰 The Best Suspense Movies on Netflix


📈 23.7 Punkte
🖥️ Betriebssysteme

🔧 How to study React to become a pro. Introduction to React.


📈 23.67 Punkte
🔧 Programmierung

🔧 Introduction to Testing React Components with Vite, Vitest and React Testing Library


📈 23.67 Punkte
🔧 Programmierung

🔧 Introduction to React Email (react.email)


📈 23.67 Punkte
🔧 Programmierung

matomo