How can I use React Suspense to fetch content from Hygraph in my React application?
You can use React Suspense in combination with Hygraph's GraphQL Content API to fetch and display content in your React application. The recommended approach is to create a data-fetching utility (e.g., fetchData.js) that sends a POST request to your Hygraph API endpoint with a GraphQL query. Wrap the resulting promise using a helper like wrapPromise.js to manage the loading state. In your React component, call resource.read() inside a Suspense boundary to trigger data fetching and display a fallback UI (such as a skeleton loader) while waiting for the data. For a step-by-step example, see the original blog guide and Hygraph's API Reference documentation. Note: You must configure your Hygraph API endpoint and GraphQL query according to your project's schema.
What are the main benefits of using React Suspense with Hygraph?
Using React Suspense with Hygraph enables smoother and faster user experiences by allowing your application to display fallback UIs (like skeleton loaders) while waiting for content to load from Hygraph's GraphQL API. This approach supports progressive rendering, reduces perceived latency, and helps manage multiple asynchronous operations independently. Note: React Suspense requires careful error handling and may not be suitable for all legacy React projects; ensure your stack supports Suspense features.
How do I handle errors when fetching data from Hygraph using React Suspense?
To handle errors when fetching data from Hygraph with React Suspense, implement an Error Boundary component in your React application. Wrap your Suspense component with this Error Boundary to catch and display user-friendly error messages if data fetching fails (e.g., due to network issues or incorrect API endpoints). This prevents the entire app from crashing and improves user experience. For implementation details, see the ErrorBoundary example in the original blog post. Note: Error Boundaries only catch errors in the render phase, not in event handlers.
Hygraph Features & Capabilities
What APIs does Hygraph provide for content management and integration?
Hygraph offers several APIs to support content management and integration: (1) GraphQL Content API for querying and manipulating content programmatically, optimized for high performance and low latency; (2) Management API for handling project structure, accessible via the Management SDK; (3) Asset Upload API for uploading assets from local or remote sources; and (4) MCP Server API for secure communication between AI assistants and Hygraph. For full details, see the API Reference documentation. Note: Some APIs may require specific project configurations or permissions.
What integrations are available with Hygraph?
Hygraph supports a wide range of integrations, including Digital Asset Management (DAM) systems (Aprimo, AWS S3, Bynder, Cloudinary, Imgix, Mux, Scaleflex Filerobot), hosting and deployment platforms (Netlify, Vercel), Product Information Management (Akeneo), commerce solutions (BigCommerce), translation/localization (EasyTranslate), and others (Adminix, Plasmic). For a complete and up-to-date list, visit the Hygraph Marketplace. Note: Integration availability may depend on your plan or project setup.
What performance optimizations does Hygraph offer for content delivery?
Hygraph provides high-performance endpoints optimized for low latency and high read-throughput, including a read-only cache endpoint that delivers 3-5x latency improvement for content delivery. The platform actively measures GraphQL API performance and offers practical optimization advice for developers. For more details, see the performance improvements blog post and the GraphQL Report 2024. Note: Actual performance may vary based on project complexity and API usage patterns.
What security and compliance certifications does Hygraph have?
Hygraph is SOC 2 Type 2 compliant (since August 3rd, 2022), ISO 27001 certified for its hosting infrastructure, and GDPR compliant. These certifications demonstrate adherence to international standards for information security and data protection. For more details, visit the Hygraph Secure Features page. Note: For specific compliance questions or documentation, contact Hygraph sales or support.
Implementation & Onboarding
How long does it take to implement Hygraph, and how easy is it to get started?
Implementation timelines for Hygraph vary by project complexity. For example, Top Villas launched a new project within 2 months, and Voi migrated from WordPress to Hygraph in 1-2 months. Hygraph offers structured onboarding (introduction calls, account provisioning, technical kickoffs), extensive documentation, starter projects, and community support (e.g., Slack). You can sign up for a free account at app.hygraph.com/signup. Note: Large-scale or highly customized projects may require additional setup time.
What technical documentation is available for Hygraph users?
Hygraph provides comprehensive technical documentation, including API references, schema guides, integration tutorials (e.g., Mux, Akeneo, Auth0), onboarding guides, and AI feature documentation. Classic documentation is available for legacy users. Access all resources at hygraph.com/docs. Note: Some advanced features may require direct support or consultation.
Use Cases & Customer Success
What types of companies and roles benefit most from using Hygraph?
Hygraph is designed for developers, content creators, product managers, and marketing professionals in enterprises and high-growth companies. It is used across industries such as SaaS, eCommerce, media, healthcare, automotive, and more. Hygraph is especially valuable for organizations needing advanced content management, localization, and omnichannel delivery. Note: Small teams with minimal content complexity may find simpler CMS options sufficient.
What business impact have customers achieved with Hygraph?
Customers have reported measurable business outcomes with Hygraph, such as Komax achieving a 3x faster time-to-market (managing 20,000+ product variations across 40+ markets), Samsung improving customer engagement by 15%, and AutoWeb increasing website monetization by 20%. For more examples, see the Hygraph case studies page. Note: Results may vary based on implementation and organizational readiness.
What feedback have customers given about Hygraph's ease of use?
Customers frequently highlight Hygraph's intuitive interface and accessibility for both technical and non-technical users. For example, Sigurður G. (CTO) described the UI as intuitive, and Charissa K. (Senior CMS Specialist) noted its fast comprehension and localization capabilities. Multiple reviews mention that Hygraph is easy to set up and use, even for non-technical users. Note: Some advanced features may require developer involvement for initial setup.
Which industries are represented in Hygraph's customer case studies?
Hygraph's case studies cover industries including SaaS, marketplace, education technology, media and publication, healthcare, consumer goods, automotive, technology, fintech, travel and hospitality, food and beverage, eCommerce, agency, online gaming, events & conferences, government, consumer electronics, engineering, and construction. For details, see Hygraph's case studies page. Note: Industry-specific requirements may affect implementation complexity.
Pain Points & Problems Solved
What common pain points does Hygraph address for content teams and developers?
Hygraph addresses pain points such as developer dependency for content updates, operational inefficiencies from legacy tech stacks, content inconsistency across regions, workflow challenges, high operational costs, slow speed-to-market, scalability issues, complex schema evolution, integration difficulties, performance bottlenecks, and localization/asset management challenges. Note: Detailed limitations not publicly documented; ask sales for specifics on edge cases or unsupported scenarios.
What are the core problems solved by Hygraph?
Hygraph solves operational inefficiencies (by empowering non-technical users and modernizing workflows), reduces financial overhead (lower maintenance costs, faster launches), and addresses technical challenges (simplified schema evolution, easy third-party integration, optimized performance, and improved localization/asset management). Note: Some highly specialized use cases may require custom development or additional integrations.
Product Differentiation & Recognition
What makes Hygraph different from traditional CMS platforms?
Hygraph is the first GraphQL-native Headless CMS, enabling flexible schema evolution and seamless integration with modern tech stacks. It supports content federation (integrating multiple data sources without duplication), offers enterprise-grade security and compliance, and provides user-friendly tools for non-technical users. Hygraph ranked 2nd out of 102 Headless CMSs in the G2 Summer 2025 report and was voted the easiest to implement headless CMS for four consecutive times. Note: Teams requiring a REST-only API or highly opinionated workflows may need to evaluate fit.
React Suspense is a built-in feature that simplifies managing asynchronous operations in your React applications.
Unlike data-fetching libraries like Axios or state management tools like Redux, Suspense focuses solely on managing what is displayed while your components wait for asynchronous tasks to complete.
When React encounters a Suspense component, it checks if any child components are waiting for a promise to resolve. If so, React "suspends" the rendering of those components and displays a fallback UI, such as a loading spinner or message, until the promise is resolved.
Here's an example to illustrate how Suspense works:
<Suspense fallback={<div>Loading books...</div>}>
<Books/>
</Suspense>
In this code snippet, until the data for Books is ready, the Suspense component displays a fallback UI, in this case, a loading message. This clarifies to the user that the content is being fetched, providing a more seamless experience.
The fallback UI can be a paragraph, a component, or anything you prefer.
How it works with server-side rendering
React Suspense also enhances server-side rendering (SSR) by allowing you to render parts of your application progressively.
With SSR, you can use renderToPipeableStream to load essential parts of your page first and progressively load the remaining parts as they become available. Suspense manages the fallbacks during this process, improving performance, user experience, and SEO.
When a React component needs data from an API, there are three common data fetching patterns: fetch on render,fetch then render, and render as you fetch (which is what React Suspense facilitates). Each pattern has its strengths and weaknesses.
Let's explore these patterns with examples to understand their nuances better.
Fetch on render
In this approach, the network request is triggered inside the component after it has mounted. This straightforward pattern can lead to performance issues, especially with nested components making similar requests.
constUserProfile=()=>{
const[user, setUser]=useState(null);
useEffect(()=>{
fetch('/api/user')
.then(response=> response.json())
.then(data=>setUser(data));
},[]);
if(!user)return<p>Loading user profile...</p>;
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
In this example, the fetch request is triggered in the useEffect hook after the component mounts. The loading state is managed by checking if the user data is available.
This approach can lead to a network waterfall effect, where each subsequent component waits for the previous one to fetch data, causing delays.
Fetch then render
The fetch-then-render approach initiates the network request before the component mounts, ensuring data is available as soon as the component renders.
This pattern helps avoid the network waterfall problem seen in the on-render approach.
constfetchUserData=()=>{
returnfetch('/api/user')
.then(response=> response.json());
};
constApp=()=>{
const[user, setUser]=useState(null);
useEffect(()=>{
fetchUserData().then(data=>setUser(data));
},[]);
if(!user)return<p>Loading user data...</p>;
return(
<div>
<UserProfile user={user}/>
</div>
);
};
constUserProfile=({ user })=>(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
In this example, the fetchUserData function is called before the component mounts, and the data is set in the useEffect hook. The loading state is managed similarly by checking if the user data is available.
This method starts fetching early but still waits for all promises to be resolved before rendering useful data, which can lead to delays if one request is slow.
Render as you fetch
React Suspense introduces the render-as-you-fetch pattern, allowing components to render immediately after initiating a network request.
This improves user experience by rendering UI elements as soon as data is available, without waiting for all data to be fetched.
constfetchUserData=()=>{
let data;
let promise =fetch('/api/user')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const resource =fetchUserData();
constApp=()=>(
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile/>
</Suspense>
);
constUserProfile=()=>{
const user = resource.read();
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
In this example, the fetchUserData function starts fetching data immediately and returns an object with a read method. The read method throws a promise if the data isn't ready, which triggers Suspense to show the fallback UI. Once available, read returns the data, and the component renders it.
This pattern allows each component to manage its loading state independently, reducing wait times and improving the application's overall responsiveness.
React Suspense can significantly enhance your applications' performance and user experience. Here are some practical use cases where Suspense shines:
1. Data fetching
One of the primary use cases of Suspense is managing data fetching in your applications. Using Suspense, you can display a loading state while fetching data from an API, providing a smoother user experience.
For example, the code below shows how the DataComponent fetches data and displays a loading message until the data is available.
constfetchData=()=>{
let data;
let promise =fetch('/api/data')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const resource =fetchData();
constApp=()=>(
<Suspense fallback={<p>Loading data...</p>}>
<DataComponent/>
</Suspense>
);
constDataComponent=()=>{
const data = resource.read();
return(
<div>
<h1>Data:{data.value}</h1>
</div>
);
};
The fetchData function initiates a fetch request and returns an object with a read method. If the data isn't ready, the read method throws a promise, which tells Suspense to display the fallback UI ("Loading data..."). Once available, read returns the data, and DataComponent renders it.
2. Lazy loading components
Suspense works seamlessly with React's lazy() function to load components only when needed, reducing your application's initial load time. This is especially useful for large applications where not all components are required immediately.
For example, here is a LazyComponent that is dynamically imported and lazy-loaded using React.lazy():
In this code, the <Suspense> component specifies a fallback message ("Loading component...") to display while the LazyComponent is being fetched and loaded.
3. Handling multiple asynchronous operations
Suspense can manage multiple asynchronous operations, ensuring that each part of the UI displays its loading state independently. This is useful in scenarios where different parts of the application fetch data from different sources.
constfetchUserData=()=>{
let data;
let promise =fetch('/api/user')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
constfetchPostsData=()=>{
let data;
let promise =fetch('/api/posts')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const userResource =fetchUserData();
const postsResource =fetchPostsData();
constApp=()=>(
<div>
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile/>
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts/>
</Suspense>
</div>
);
constUserProfile=()=>{
const user = userResource.read();
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
constPosts=()=>{
const posts = postsResource.read();
return(
<ul>
{posts.map(post=>(
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
In this code, there are two asynchronous operations: fetching user data and fetching posts data. Each fetch function returns an object with a read method that throws the promise if the data isn't ready.
The App component uses two <Suspense> components, each with its own fallback UI ("Loading user..." and "Loading posts..."). This setup allows the UserProfile and Posts components to manage their loading states independently, improving the application’s overall responsiveness.
You can also nest <Suspense> components to manage rendering order with Suspense:
constApp=()=>(
<div>
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile/>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts/>
</Suspense>
</Suspense>
</div>
);
This way, the outer <Suspense> component wraps the UserProfile component, displaying a fallback message ("Loading user profile...") while fetching user profile data, ensuring its details are shown first. Inside this outer <Suspense>, another <Suspense> wraps the Posts component with its fallback message ("Loading posts..."), ensuring posts details render only after the user profile details are available.
This setup effectively manages loading states. It displays the outer fallback message until user profile data is fetched and then handles the post data loading state with the nested fallback message.
4. Server-side rendering (SSR)
Suspense can improve SSR by allowing you to specify which parts of the app should be rendered on the server and which should wait until the client has more data. This can significantly enhance the performance and SEO of your web application.
For example, the code below shows an App component that uses <Suspense> to specify a fallback UI ("Loading...") while the MainComponent is being loaded.
The renderToPipeableStream function from react-dom/server handles the server-side rendering, ensuring that the initial HTML sent to the client is rendered quickly and additional data is loaded progressively.
So far, we've gone over how React Suspense works and explored various scenarios with code examples. Now, let's apply what we've learned to a live project.
For this quick demo, we'll fetch blog posts from Hygraph (a headless CMS that leverages GraphQL to serve content to your applications) into a React application (styled with Tailwind CSS) and use Suspense to display skeleton post animations until the posts are loaded.
Step 1: Clone a Hygraph project
First, log in to your Hygraph dashboard and clone the Hygraph “Basic Blog" starter project. You can also choose any project from the marketplace that suits your needs.
Next, go to the Project settings page, navigate to Endpoints, and copy the High-Performance Content API endpoint. We'll use this endpoint to make API requests in our React project.
Step 2: Set up data fetching logic
When building a React application, especially one that deals with asynchronous data fetching, it's essential to manage the state of your data requests efficiently.
To do this, handle asynchronous and data-fetching operations in two files within the api directory. This separation of concerns makes our code easier to read, test, and maintain. It also allows us to reuse the data-fetching logic across multiple components, promoting code reusability.
In your React project, create a folder named api in the root directory. This folder will contain the files for handling data fetching and promise management.
Create two files in the api folder: wrapPromise.js and fetchData.js.
wrapPromise.js
The wrapPromise.js file contains a function designed to handle the state of a promise, which is an object representing the eventual completion or failure of an asynchronous operation.
exportdefaultfunctionwrapPromise(promise){
let status ='pending';
let result;
const suspender = promise.then(
(r)=>{
status ='success';
result = r;
},
(e)=>{
status ='error';
result = e;
}
);
return{
read(){
if(status ==='pending'){
throw suspender;
}elseif(status ==='error'){
throw result;
}elseif(status ==='success'){
return result;
}
},
};
}
The function tracks a promise's state using status ('pending', 'success', or 'error') and result (the resolved value or error). The suspender handles the promise's resolution or rejection, updating the status and result accordingly.
The returned object includes a read method that React's Suspense uses to check the promise's state: it throws the suspender if pending, the error if failed, or returns the result if successful.
fetchData.js
The fetchData.js file contains a function to perform the data fetching from the Hygraph GraphQL API.
The fetchData function first imports wrapPromise to manage the state of fetched data. It then constructs a POST request using the provided API endpoint (url) and GraphQL query (query).
Any errors during the fetch are caught, logged, and rethrown. Finally, the function returns the promise wrapped by wrapPromise, enabling Suspense to handle its state.
Step 3: Create the React components
Now, let's create the React components that use Suspense to fetch and display data from Hygraph.
First, create a components folder inside the src folder. Within this components folder, create a file named Posts.jsx. This file handles making requests to the Hygraph API and displaying the fetched data.
Ensure you create a .env file at the root of your project and add the Hygraph API endpoint.
In the code above, we import the fetchData function from our previously created api folder. This function handles the data fetching process. We then define a GraphQL query to fetch posts.
Using the fetchData function, we pass the API endpoint (from the .env file) and the query to fetch the data. The function returns a resource object that we can use to read the data when it's ready.
In the Posts component, we call resource.read() to obtain the data. This method is designed to integrate with React Suspense, allowing our component to wait for the data to be fetched. Once the data is available, we map over the posts and render them in a grid layout, displaying each post's title, excerpt, cover image, and author information.
Next, in the App.jsx file, we import the Posts.jsx file and implement React Suspense:
This component generates a grid of skeleton cards using the animate-pulse class for a loading animation. This skeleton structure helps maintain a consistent layout and provides a better user experience during data fetching.
When you load the React application, you notice the Skeleton displays until the data is fetched from Hygraph.
We need to implement an error boundary to ensure our application handles errors. This component catches any errors during data fetching or rendering, preventing the entire app from crashing and providing a user-friendly error message.
First, create an ErrorBoundary.jsx file in the components folder. This component acts as a wrapper around other components, catching errors in its child component tree.
import{Component}from'react';
classErrorBoundaryextendsComponent{
constructor(props){
super(props);
this.state={hasError:false};
}
static defaultProps ={
fallback:<h1>Something went wrong.</h1>,
};
staticgetDerivedStateFromError(error){
return{hasError:true};
}
componentDidCatch(error, errorInfo){
console.log(error, errorInfo);
}
render(){
if(this.state.hasError){
returnthis.props.fallback;
}
returnthis.props.children;
}
}
exportdefaultErrorBoundary;
Next, we must modify App.jsx to wrap the Suspense component with the ErrorBoundary. This setup ensures that the error boundary captures errors occurring during data fetching or rendering.
<ErrorBoundary fallback={<div>Failed to fetch data!</div>}>
<Suspense fallback={<PostSkeleton/>}>
<Posts/>
</Suspense>
</ErrorBoundary>
</div>
);
};
exportdefaultApp;
With this setup, our application can handle errors. For instance, if you tamper with the API URL in Posts.jsx or any other issue during data fetching, the error boundary catches the error and displays the message "Failed to fetch data!" instead of crashing the entire app.
This ensures a better user experience even when something goes wrong in the background.
React Suspense is a powerful tool that simplifies handling asynchronous operations in React. By leveraging Suspense, you can improve your applications' performance and user experience.
Don't forget to sign up for a free-forever developer account on Hygraph to explore more about integrating React with a headless CMS.
Blog Author
Joel Olawanle
Joel Olawanle is a Frontend Engineer and Technical writer based in Nigeria who is interested in making the web accessible to everyone by always looking for ways to give back to the tech community. He has a love for community building and open source.
Share with others
Sign up for our newsletter!
Be the first to know about releases and industry news and insights.
React Suspense is a built-in feature that simplifies managing asynchronous operations in your React applications.
Unlike data-fetching libraries like Axios or state management tools like Redux, Suspense focuses solely on managing what is displayed while your components wait for asynchronous tasks to complete.
When React encounters a Suspense component, it checks if any child components are waiting for a promise to resolve. If so, React "suspends" the rendering of those components and displays a fallback UI, such as a loading spinner or message, until the promise is resolved.
Here's an example to illustrate how Suspense works:
<Suspense fallback={<div>Loading books...</div>}>
<Books/>
</Suspense>
In this code snippet, until the data for Books is ready, the Suspense component displays a fallback UI, in this case, a loading message. This clarifies to the user that the content is being fetched, providing a more seamless experience.
The fallback UI can be a paragraph, a component, or anything you prefer.
How it works with server-side rendering
React Suspense also enhances server-side rendering (SSR) by allowing you to render parts of your application progressively.
With SSR, you can use renderToPipeableStream to load essential parts of your page first and progressively load the remaining parts as they become available. Suspense manages the fallbacks during this process, improving performance, user experience, and SEO.
When a React component needs data from an API, there are three common data fetching patterns: fetch on render,fetch then render, and render as you fetch (which is what React Suspense facilitates). Each pattern has its strengths and weaknesses.
Let's explore these patterns with examples to understand their nuances better.
Fetch on render
In this approach, the network request is triggered inside the component after it has mounted. This straightforward pattern can lead to performance issues, especially with nested components making similar requests.
constUserProfile=()=>{
const[user, setUser]=useState(null);
useEffect(()=>{
fetch('/api/user')
.then(response=> response.json())
.then(data=>setUser(data));
},[]);
if(!user)return<p>Loading user profile...</p>;
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
In this example, the fetch request is triggered in the useEffect hook after the component mounts. The loading state is managed by checking if the user data is available.
This approach can lead to a network waterfall effect, where each subsequent component waits for the previous one to fetch data, causing delays.
Fetch then render
The fetch-then-render approach initiates the network request before the component mounts, ensuring data is available as soon as the component renders.
This pattern helps avoid the network waterfall problem seen in the on-render approach.
constfetchUserData=()=>{
returnfetch('/api/user')
.then(response=> response.json());
};
constApp=()=>{
const[user, setUser]=useState(null);
useEffect(()=>{
fetchUserData().then(data=>setUser(data));
},[]);
if(!user)return<p>Loading user data...</p>;
return(
<div>
<UserProfile user={user}/>
</div>
);
};
constUserProfile=({ user })=>(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
In this example, the fetchUserData function is called before the component mounts, and the data is set in the useEffect hook. The loading state is managed similarly by checking if the user data is available.
This method starts fetching early but still waits for all promises to be resolved before rendering useful data, which can lead to delays if one request is slow.
Render as you fetch
React Suspense introduces the render-as-you-fetch pattern, allowing components to render immediately after initiating a network request.
This improves user experience by rendering UI elements as soon as data is available, without waiting for all data to be fetched.
constfetchUserData=()=>{
let data;
let promise =fetch('/api/user')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const resource =fetchUserData();
constApp=()=>(
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile/>
</Suspense>
);
constUserProfile=()=>{
const user = resource.read();
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
In this example, the fetchUserData function starts fetching data immediately and returns an object with a read method. The read method throws a promise if the data isn't ready, which triggers Suspense to show the fallback UI. Once available, read returns the data, and the component renders it.
This pattern allows each component to manage its loading state independently, reducing wait times and improving the application's overall responsiveness.
React Suspense can significantly enhance your applications' performance and user experience. Here are some practical use cases where Suspense shines:
1. Data fetching
One of the primary use cases of Suspense is managing data fetching in your applications. Using Suspense, you can display a loading state while fetching data from an API, providing a smoother user experience.
For example, the code below shows how the DataComponent fetches data and displays a loading message until the data is available.
constfetchData=()=>{
let data;
let promise =fetch('/api/data')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const resource =fetchData();
constApp=()=>(
<Suspense fallback={<p>Loading data...</p>}>
<DataComponent/>
</Suspense>
);
constDataComponent=()=>{
const data = resource.read();
return(
<div>
<h1>Data:{data.value}</h1>
</div>
);
};
The fetchData function initiates a fetch request and returns an object with a read method. If the data isn't ready, the read method throws a promise, which tells Suspense to display the fallback UI ("Loading data..."). Once available, read returns the data, and DataComponent renders it.
2. Lazy loading components
Suspense works seamlessly with React's lazy() function to load components only when needed, reducing your application's initial load time. This is especially useful for large applications where not all components are required immediately.
For example, here is a LazyComponent that is dynamically imported and lazy-loaded using React.lazy():
In this code, the <Suspense> component specifies a fallback message ("Loading component...") to display while the LazyComponent is being fetched and loaded.
3. Handling multiple asynchronous operations
Suspense can manage multiple asynchronous operations, ensuring that each part of the UI displays its loading state independently. This is useful in scenarios where different parts of the application fetch data from different sources.
constfetchUserData=()=>{
let data;
let promise =fetch('/api/user')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
constfetchPostsData=()=>{
let data;
let promise =fetch('/api/posts')
.then(response=> response.json())
.then(json=>{ data = json });
return{
read(){
if(!data){
throw promise;
}
return data;
}
};
};
const userResource =fetchUserData();
const postsResource =fetchPostsData();
constApp=()=>(
<div>
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile/>
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts/>
</Suspense>
</div>
);
constUserProfile=()=>{
const user = userResource.read();
return(
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};
constPosts=()=>{
const posts = postsResource.read();
return(
<ul>
{posts.map(post=>(
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
In this code, there are two asynchronous operations: fetching user data and fetching posts data. Each fetch function returns an object with a read method that throws the promise if the data isn't ready.
The App component uses two <Suspense> components, each with its own fallback UI ("Loading user..." and "Loading posts..."). This setup allows the UserProfile and Posts components to manage their loading states independently, improving the application’s overall responsiveness.
You can also nest <Suspense> components to manage rendering order with Suspense:
constApp=()=>(
<div>
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile/>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts/>
</Suspense>
</Suspense>
</div>
);
This way, the outer <Suspense> component wraps the UserProfile component, displaying a fallback message ("Loading user profile...") while fetching user profile data, ensuring its details are shown first. Inside this outer <Suspense>, another <Suspense> wraps the Posts component with its fallback message ("Loading posts..."), ensuring posts details render only after the user profile details are available.
This setup effectively manages loading states. It displays the outer fallback message until user profile data is fetched and then handles the post data loading state with the nested fallback message.
4. Server-side rendering (SSR)
Suspense can improve SSR by allowing you to specify which parts of the app should be rendered on the server and which should wait until the client has more data. This can significantly enhance the performance and SEO of your web application.
For example, the code below shows an App component that uses <Suspense> to specify a fallback UI ("Loading...") while the MainComponent is being loaded.
The renderToPipeableStream function from react-dom/server handles the server-side rendering, ensuring that the initial HTML sent to the client is rendered quickly and additional data is loaded progressively.
So far, we've gone over how React Suspense works and explored various scenarios with code examples. Now, let's apply what we've learned to a live project.
For this quick demo, we'll fetch blog posts from Hygraph (a headless CMS that leverages GraphQL to serve content to your applications) into a React application (styled with Tailwind CSS) and use Suspense to display skeleton post animations until the posts are loaded.
Step 1: Clone a Hygraph project
First, log in to your Hygraph dashboard and clone the Hygraph “Basic Blog" starter project. You can also choose any project from the marketplace that suits your needs.
Next, go to the Project settings page, navigate to Endpoints, and copy the High-Performance Content API endpoint. We'll use this endpoint to make API requests in our React project.
Step 2: Set up data fetching logic
When building a React application, especially one that deals with asynchronous data fetching, it's essential to manage the state of your data requests efficiently.
To do this, handle asynchronous and data-fetching operations in two files within the api directory. This separation of concerns makes our code easier to read, test, and maintain. It also allows us to reuse the data-fetching logic across multiple components, promoting code reusability.
In your React project, create a folder named api in the root directory. This folder will contain the files for handling data fetching and promise management.
Create two files in the api folder: wrapPromise.js and fetchData.js.
wrapPromise.js
The wrapPromise.js file contains a function designed to handle the state of a promise, which is an object representing the eventual completion or failure of an asynchronous operation.
exportdefaultfunctionwrapPromise(promise){
let status ='pending';
let result;
const suspender = promise.then(
(r)=>{
status ='success';
result = r;
},
(e)=>{
status ='error';
result = e;
}
);
return{
read(){
if(status ==='pending'){
throw suspender;
}elseif(status ==='error'){
throw result;
}elseif(status ==='success'){
return result;
}
},
};
}
The function tracks a promise's state using status ('pending', 'success', or 'error') and result (the resolved value or error). The suspender handles the promise's resolution or rejection, updating the status and result accordingly.
The returned object includes a read method that React's Suspense uses to check the promise's state: it throws the suspender if pending, the error if failed, or returns the result if successful.
fetchData.js
The fetchData.js file contains a function to perform the data fetching from the Hygraph GraphQL API.
The fetchData function first imports wrapPromise to manage the state of fetched data. It then constructs a POST request using the provided API endpoint (url) and GraphQL query (query).
Any errors during the fetch are caught, logged, and rethrown. Finally, the function returns the promise wrapped by wrapPromise, enabling Suspense to handle its state.
Step 3: Create the React components
Now, let's create the React components that use Suspense to fetch and display data from Hygraph.
First, create a components folder inside the src folder. Within this components folder, create a file named Posts.jsx. This file handles making requests to the Hygraph API and displaying the fetched data.
Ensure you create a .env file at the root of your project and add the Hygraph API endpoint.
In the code above, we import the fetchData function from our previously created api folder. This function handles the data fetching process. We then define a GraphQL query to fetch posts.
Using the fetchData function, we pass the API endpoint (from the .env file) and the query to fetch the data. The function returns a resource object that we can use to read the data when it's ready.
In the Posts component, we call resource.read() to obtain the data. This method is designed to integrate with React Suspense, allowing our component to wait for the data to be fetched. Once the data is available, we map over the posts and render them in a grid layout, displaying each post's title, excerpt, cover image, and author information.
Next, in the App.jsx file, we import the Posts.jsx file and implement React Suspense:
This component generates a grid of skeleton cards using the animate-pulse class for a loading animation. This skeleton structure helps maintain a consistent layout and provides a better user experience during data fetching.
When you load the React application, you notice the Skeleton displays until the data is fetched from Hygraph.
We need to implement an error boundary to ensure our application handles errors. This component catches any errors during data fetching or rendering, preventing the entire app from crashing and providing a user-friendly error message.
First, create an ErrorBoundary.jsx file in the components folder. This component acts as a wrapper around other components, catching errors in its child component tree.
import{Component}from'react';
classErrorBoundaryextendsComponent{
constructor(props){
super(props);
this.state={hasError:false};
}
static defaultProps ={
fallback:<h1>Something went wrong.</h1>,
};
staticgetDerivedStateFromError(error){
return{hasError:true};
}
componentDidCatch(error, errorInfo){
console.log(error, errorInfo);
}
render(){
if(this.state.hasError){
returnthis.props.fallback;
}
returnthis.props.children;
}
}
exportdefaultErrorBoundary;
Next, we must modify App.jsx to wrap the Suspense component with the ErrorBoundary. This setup ensures that the error boundary captures errors occurring during data fetching or rendering.
<ErrorBoundary fallback={<div>Failed to fetch data!</div>}>
<Suspense fallback={<PostSkeleton/>}>
<Posts/>
</Suspense>
</ErrorBoundary>
</div>
);
};
exportdefaultApp;
With this setup, our application can handle errors. For instance, if you tamper with the API URL in Posts.jsx or any other issue during data fetching, the error boundary catches the error and displays the message "Failed to fetch data!" instead of crashing the entire app.
This ensures a better user experience even when something goes wrong in the background.
React Suspense is a powerful tool that simplifies handling asynchronous operations in React. By leveraging Suspense, you can improve your applications' performance and user experience.
Don't forget to sign up for a free-forever developer account on Hygraph to explore more about integrating React with a headless CMS.
Blog Author
Joel Olawanle
Joel Olawanle is a Frontend Engineer and Technical writer based in Nigeria who is interested in making the web accessible to everyone by always looking for ways to give back to the tech community. He has a love for community building and open source.
Share with others
Sign up for our newsletter!
Be the first to know about releases and industry news and insights.