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
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.
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;
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.
Without the network throttle, the app loads faster than before.
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)
Please write the code blog like
It shows your code with color and It will be easy to read.
Thanks I will try
you know we really need a better guide for stuff like that
github.com/adam-p/markdown-here/wi...
It is a good reference to markup text.
thx