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
If you are looking for a React Native Chat SDK to build in app chat messaging application, you can consider DeadSimpleChat React Native SDK
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.
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 then 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 an 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 boundries and makes for a more modular and declarative approch to data fetching in react suspense
Let us consider an example to better understand this
- Let us consider an Object with name resource, which is responsable for data catching and suspense integration
2. Now, we need to create a function that would update the resource object whenever it gets the data
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 needs 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 suspend 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, non descriptive error messages, or unexpected behaviour 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 on how to create an error boundry 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 boundry 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 fethcing the data by using theuseFetch
custom hook - If 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 displays the alternate UI - This 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 occuring 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 data - Then 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 server - And In the
App
Component we are wrapping thePost
Component with Suspense so as to load the loading state - We 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 hoook 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 a input field. that will filter the posts by the title
function App() {
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const handleSearch = (event) => {
setSearchTerm(event.target.value);
setPage(1);
};
return (
<div>
<h1>React Suspense with Pagination and Filters</h1>
<input
type="text"
placeholder="Search by title..."
value={searchTerm}
onChange={handleSearch}
/>
<Suspense fallback={<div>Loading...</div>}>
<Posts page={page} params={{ q: searchTerm }} />
</Suspense>
<button
onClick={() => setPage((prevPage) => prevPage - 1)}
disabled={page === 1}
>
Previous
</button>
<button onClick={() => setPage((prevPage) => prevPage + 1)}>Next</button>
</div>
);
}
Let us consider what we are doing the above example
- We are modifing the
usePaginatedFetch
hook to accept aparams
Object - In the
App
component we have added a input field to allow users to search for a term - We have also created a
handleSearch
function to handle thesearchTerm
state whenever the input value changes that is whenever a user writes into the input field - We are passing the
searchTerm
into theparams
props as a query parameter to the posts component - the
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 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
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
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
import React, { lazy, Suspense } from "react";
const Posts = lazy(() => import("./Posts"));
function App() {
return (
<div>
<h1>React Suspense with Code Splitting</h1>
<Suspense fallback={<div>Loading...</div>}>
<Posts />
</Suspense>
</div>
);
}
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 app using the react dev tools
Need Chat API for your website or app
DeadSimpleChat is an 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