Data Fetching with React Suspense
alakkadshaw
Posted on August 27, 2023
Introduction
What is React suspense?
Importance of data fetching in react apps
Fetching Data with React Suspense
Data Fetching in React Suspense with custom hooks
Example and How to create custom Hook
Error Boundaries
Importance of handling errors
Creating Error Boundary Components
Integrating Error Boundaries with Suspense Fallbacks
Example Code
Real world use cases
1. Fetch on Render
2. Fetch-then-Render
3. Render-as-you-Fetch
Implementing Pagination with React Suspense
Adding Filter Functionality
Example
Best Practices and Performance Considerations
Disabling Suspense for slow networks
Code Splitting
Data-caching strategies
Profiling and optimization
React Suspense and Concurrent UI
What is the Concurrent Mode?
Advantages of using Concurrent UI with React Suspense
Example
Conclusion
Introduction
React Suspense is a built in react component that lets you display a fallback until its children have finished loading
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
React Suspense simplifies the handling of asynchronous data fetching and rendering in react apps thus helping the developer to enable them to declaratively specify loading states, error handling.
For almost all web and mobile applications, one of the most important aspects if data fetching from a remote server.
In React applications typically we have components that fetch the data by using API, then after fetching they process the data and after the processing is done the data is rendered on the screen
React Suspense provides a more intuitive and easy to maintain way to fetch and render data on screen.
Here is a simple example of React Suspense in action
- We are creating a custom hook to fetch the data from the server. We are calling it
useDataFetch
this
import { useState, useEffect } from "react";
function useDataFetch(url) {
const [data, setData] = useState(null);
const [searching, setSearching] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function getData() {
try {
const response = await fetch(url);
const resultantData = await response.json();
setData(resultantData);
} catch (err) {
setError(err);
} finally {
setSearching(false);
}
}
getData();
}, [url]);
return { data, searching, error };
}
2. We are then using the data returned by the useDataFetch
hook with React Suspense
import React, { Suspense } from 'react';
import { useFetch } from './useFetch';
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (loading) {
throw new Promise(resolve => setTimeout(resolve, 1000));
}
if (error) {
throw error;
}
return <div>{data.name}</div>;
}
function App({ userId }) {
return (
<div>
<Suspense fallback={<div>Getting User Data </div>}>
<UserProfile userId={userId} />
</Suspense>
</div>
);
}
In this example we have a UserProfile Component that is getting the data from the useDataFetch
custom hook
Fetching Data with React Suspense and Custom Hooks
To Fetch data using React Suspense we need to create custom hooks.
These custom hooks will allow us to suspend the components while the data is retrieved
React Suspense streamlines the loading states and error boundaries and makes for a more modular and declarative approach to data fetching in React suspense
Let us consider an example to better understand this
- Let us consider an Object with a name resource, which is responsible for data catching and suspense integration
const resource = {
data: null,
promise: null,
};
resource Object
2. Now, we need to create a function that would update the resource object whenever it gets the data
const fetchData = async (url) => {
resource.promise = fetch(url)
.then((response) => response.json())
.then((data) => {
resource.data = data;
resource.promise = null;
});
throw resource.promise;
};
fetchData function
3. Lastly we create a custom hook that we will integrate with React Suspense
import { useState, useEffect } from "react";
function useDataFetch(url) {
const [data, setData] = useState(resource.data);
useEffect(() => {
if (!resource.promise) {
fetchData(url);
}
let promise = resource.promise;
promise
.then((data) => setData(data))
.catch((error) => {
//take care of the error here
});
}, [url]);
if (resource.data) {
return data;
} else {
throw resource.promise;
}
}
When the useDataFetch
hook is called the component will stop rendering on the screen and then only when the data is available the compoenent will start rendering again
Integrating Custom Hooks with Suspense
In this section we will integrate the custom react hook that we created called the useFetch
into our component
import React, { Suspense } from "react";
import { useDataFetch } from "./useDataFetch";
function Post({ id }) {
const post = useFetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Getting the data from React</h1>
<Suspense fallback={<div>It is taking some time to get the data</div>}>
<Post id={1} />
</Suspense>
</div>
);
}
Error Boundaries: A Simple Example
The errors that happen in the front-end applications need to be handled gracefully.
Creating an error boundary component allows the developer to catch any errors thrown by the suspended component
let us look at an example to understand this better
import React from "react";
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, errorInfo) {
// you can handle the errors here
}
render() {
const { error } = this.state;
const { children, fallback } = this.props;
if (error) {
return fallback ? fallback(error) : null;
}
return children;
}
}
We need to wrap the components that are likely to throw errors inside the ErrorBoundary
component that we created to it to be able to catch the errors
function App() {
return (
<div>
<h1>Fetching Data with React Suspense</h1>
<ErrorBoundary fallback={(error) => <div>Error: {error.message}</div>}>
<Suspense fallback={<div>Loading post...</div>}>
<Post id={1} />
</Suspense>
</ErrorBoundary>
</div>
);
}
Fetching data with react suspense takes creating custom hooks that fetch data and integrating them with components
These components then suspended during data fetching.
ErrorBoundries can be deployed to catch any error which might be thrown by the suspended components
Why Error handling is important
We need to application to perform even if problems come during run time.
We are looking to avoid issues like crashing, nondescriptive error messages, or unexpected behavior that would really frustrate the user
This article is brought to you by DeadSimpleChat, Chat API and SDK for your website and app
Better error handling results in developers being able to quickly resolve issues and users getting relevant messages as to why the app is not working if it has stopped working.
Enhances user experiences and makes the application more robust
Creating error-handling boundary components
ErrorBoundry
components catch errors anywhere in its child components, logs those errors and show the fallback UI
Let us consider an example of how to create an error boundary component
import { useState, useEffect } from 'react';
function useErrorBoundary() {
const [error, setError] = useState(null);
const handleError = (err) => {
setError(err);
};
useEffect(
() =>
function cleanup() {
setError(null);
},
[]
);
return [error, handleError];
}
Here we are creating an error boundary hook, next We will create a ErrorBoundry
component below
import React from 'react';
import { useErrorBoundary } from './useErrorBoundary';
function ErrorBoundary({ children, fallback }) {
const [error, handleError] = useErrorBoundary();
if (error) {
return fallback ? fallback(error) : null;
}
return (
<React.ErrorBoundary onError={handleError}>{children}</React.ErrorBoundary>
);
}
Integrating the Error boundry Hook with the react component and react suspense fallback
In this case, if the component fails then the suspense fallback will display an alternate UI to the user
import React, { Suspense } from 'react';
import { useFetch } from './useFetch';
import ErrorBoundary from './ErrorBoundary';
function UserProfile({ userId }) {
const { data, error } = useFetch(`https://api.example.com/users/${userId}`);
if (error) {
throw error;
}
return <div>{data.name}</div>;
}
function App() {
return (
<div>
<h1>Data Fetching with React Suspense</h1>
<ErrorBoundary fallback={(error) => <div>Error: {error.message}</div>}>
<Suspense fallback={<div>Loading user profile...</div>}>
<UserProfile userId={1} />
</Suspense>
</ErrorBoundary>
</div>
);
}
In the above example
The
userProfile
component has the job of fetching the data by using theuseFetch
custom hookIf there is some error in fetching the data then the suspended component throws an error.
Which is caught by the
ErrorBoundry
component and the suspense fallback display the alternate UIThis leads to better user experience and developer experience as well
Real World use-cases
Data fetch and render patterns
Fetch on render
fetch then render
render as you fetch
Fetch-On-Render
In fetch on render, the component renders placeholder and loading states while requesting the data as they mount.
Data fetching starts as soon as the component is rendered and is blocked (that is placeholders are shown) until the data arrives
Let us look at the example
import React, { useState, useEffect } from "react";
function BlogPost({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const postData = await response.json();
setPost(postData);
})();
}, [postId]);
if (!post) {
return <div>Loading...</div>;
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
2. Fetch-then-render
In this render pattern, the component is only shown when all the data is available for render
here no placeholder or loading state is shown, this may cause initial render times to slow down
But it is preferable for some kinds of use cases
import React, { useState, useEffect } from "react";
function BlogPosts() {
const [posts, setPosts] = useState(null);
useEffect(() => {
(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10"
);
const postData = await response.json();
setPosts(postData);
})();
}, []);
if (!posts) {
return <div>Loading...</div>;
}
return (
<div>
{posts.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))}
</div>
);
}
3. Render as you fetch
Here the data is rendered as soon as it is coming from the server. Here the data fetching and rendering are occurring at the same time
As soon as some of the data is available it is rendered by the component while waiting for the additional data from the server
This technique combines React Suspense with the custom hooks to better manage the async process
import React, { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
setData(null);
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result);
});
}, [url]);
if (data === null) {
throw new Promise((resolve) => setTimeout(resolve, 2000));
}
return data;
}
function BlogPost({ postId }) {
const post = useFetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render as you fetch</h1>
<React.Suspense fallback={<div>please wait while we are searching</div>}>
<BlogPost postId={1} />
</React.Suspense>
</div>
);
}
These are some of the real-world fetching patterns available in react suspense
You can use them according to the nature of your react application and the amount of data.
Implementing Pagination and filtering for React Suspense
Let us learn how to implement pagination and filtering with react suspense
We are going to combine custom hooks and react suspense components to implment pagination
Pagination
Let us create a custom hook named usePaginatedFetch
which will retrive the data based of the below parameters
URL
and
page
import { useState, useEffect } from "react";
function usePaginatedFetch(url, page) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`${url}?_page=${page}&_limit=10`)
.then((response) => response.json())
.then((result) => {
setData(result);
setLoading(false);
});
}, [url, page]);
if (loading) {
throw new Promise((resolve) => setTimeout(resolve, 2000));
}
return data;
}
Next, we will create a new component and name that Posts
component then we will use the usePaginatedFetch
component to display the data
import React, { Suspense, useState } from "react";
import { usePaginatedFetch } from "./usePaginatedFetch";
function Posts({ page }) {
const posts = usePaginatedFetch(
"https://jsonplaceholder.typicode.com/posts",
page
);
return (
<div>
{posts.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))}
</div>
);
}
function App() {
const [page, setPage] = useState(1);
return (
<div>
<h1>React Suspense Pagination</h1>
<Suspense fallback={<div>Please wait while we search for the page</div>}>
<Posts page={page} />
</Suspense>
<button onClick={() => setPage((prevPage) => prevPage - 1)} disabled={page === 1}>
older Page
</button>
<button onClick={() => setPage((prevPage) => prevPage + 1)}>Next Page</button>
</div>
);
}
Again let us revisit what we did in the above example
We created a custom hook
usePaginatedData
which would fetch the data from the server using the paramsURL
and page`The state of the fetched data and the loading status are maintained by the custom hook
In our code when the loading state is set to true then the react signals the component to skip rendering and wait for the data and the
usePaginatedFetch
sends a promise to fetch the dataThen we created the
Posts
component that takes apage
as a prop and renders a list of posts based on the data that is returned by theusePaginatedData
custom hook. Data is fetched from the remote serverAnd In the
App
Component we are wrapping thePost
Component with Suspense so as to load the loading stateWe have also created 2 button components to handle pagination.
Filters
Let us add the filter functionality. To do this we need to modify the usePaginatedFetch
custom hook to accept a new params
Object
Let us look at the code
`
function usePaginatedFetch(url, page, params) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const queryParams = new URLSearchParams({
...params,
_page: page,
_limit: 20,
});
`
useEffect(() => {
setLoading(true);
fetch(${url}?${queryParams})
.then((response) => response.json())
.then((result) => {
setData(result);
setLoading(false);
});
}, [url, page, params]);
if (loading) {
throw new Promise((resolve) => setTimeout(resolve, 2000));
}
return data;
}
`
Let us also update the App
component to have an input field. that will filter the posts by the title
`javascript
function App() {
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const handleSearch = (event) => {
setSearchTerm(event.target.value);
setPage(1);
};
return (
React Suspense with Pagination and Filters
type="text"
placeholder="Search by title..."
value={searchTerm}
onChange={handleSearch}
/>
Loading...}>
onClick={() => setPage((prevPage) => prevPage - 1)}
disabled={page === 1}
>
Previous
setPage((prevPage) => prevPage + 1)}>Next
);
}
`
Let us consider what we are doing the above example
We are modifying the
usePaginatedFetch
hook to accept aparams
ObjectIn the
App
component we have added an input field to allow users to search for a termWe have also created a
handleSearch
function to handle thesearchTerm
state whenever the input value changes that is whenever a user writes into the input fieldWe are passing the
searchTerm
into theparams
props as a query parameter to the post componentthe
usePaginatedFetch
now includes the search term in the API request. This allows the server to filter the results using the search term
Best Practices and Performance Considerations
- Disabling Suspense for slow networks
When the app is on a slow network, it might be better to show a loading screen rather than waiting for a fallback UI.
You can disable suspense for slow networks by using the
navigator.connection
API to detect the user's network speed
`javascript
function isSlowNetwork() {
return (
navigator.connection &&
(["slow-2g", "2g"].includes(navigator.connection.effectiveType) ||
navigator.connection.saveData)
);
}
`
When we get to know that the app is on a slow network then we can conditionally render the component without the suspense
wrapper
`javascript
function App() {
if (isSlowNetwork()) {
return <Posts />;
} else {
return (
<div>
<h1>React Suspense tutorial</h1>
<Suspense fallback={<div>Searching...</div>}>
<Posts />
</Suspense>
</div>
);
}
}
`
Code Splitting
With code splitting, you can separate your application into smaller chunks.
These chunks can be loaded only when they are needed thus saving on resources
For example: loading the resources as the user navigates through the app
For code splitting, you can use
React.lazy
`javascript
import React, { lazy, Suspense } from "react";
const Posts = lazy(() => import("./Posts"));
function App() {
return (
React Suspense with Code Splitting
Loading...}>
);
}
`
Data-caching strategies
You can improve the performance of an application by caching the data fetched from the server.
Profiling and Optimizing
Using React dev tools we can profile and optimize the application
We can check the component renders and find out the performance bottlenecks.
Thus we can optimize the app using the React Dev tools
Need Chat API for your website or app
DeadSimpleChat is a Chat API provider
Add Scalable Chat to your app in minutes
10 Million Online Concurrent users
99.999% Uptime
Moderation features
1-1 Chat
Group Chat
Fully Customizable
Chat API and SDK
Pre-Built Chat
Conclusion
In this article we learnt about React Suspense and how we can use React suspense to fetch data by creating a custom hook
I hope you liked the article Thank you for reading
Posted on August 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 2, 2024