React 18 Suspense fetch data from a headless CMS
Naira Gezhoyan
Posted on August 19, 2022
To deliver a great website user experience, we need to optimize the first initial page load time and the page's responsiveness to interaction. The faster your page responds to user input – the better.
React 18 was designed to help improve interactivity with features like selective hydration with Suspense
to make hydration non-blocking and provide us with more transparency about how our architectural choices will affect our app's UX and performance. React 18 makes major performance improvements and adds support for Suspense
on server-side rendering (SSR) allowing serving parts of an app asynchronously possible, you can wrap a slow part of your app within the Suspense component, telling React to delay the loading of the slow component.
Server-side rendering lets you generate HTML from React components on the server, and send that HTML to your users. SSR lets your users see the page’s content before your JavaScript bundle loads and runs, after which the JavaScript code loads and merges with the HTML, attaching event handlers - which is hydration. Unlike traditional HTML streaming, it doesn’t have to happen in the top-down order.
With Suspense
, you can tell React to send HTML for other components first along with the HTML for the placeholder, like a loading spinner. It significantly improves the user experience and user-perceived latency.
There are two major SSR features in React 18 unlocked by Suspense:
- Streaming HTML on the server.
- Selective Hydration on the client.
Let’s explore React Data fetching approaches with useEffect
and Suspense
try to compare backend data fetching practical solutions, in our case we choose a fast and intuitive headless CMS Cosmic. Our code examples you can check by a link StackBlitz.
Integration Cosmic Headless CMS
For fetching data we use Cosmic headless CMS is a back-end-only content management system (CMS), which is built from the ground up a content repository that makes content accessible. To integrate and get values from Cosmic, we need to install the Cosmic module in your project.
npm i cosmicjs
# or
yarn add cosmicjs
Then create a free Cosmic account and go to Cosmic Dashboard Your Bucket > Settings > API Access
and find your Bucket slug and API read key and add them to creating Cosmic fetch function fetchDataByType
request to your Cosmic bucket and fetch created Categories content by Cosmic query type categories
.
// cosmic.js
import Cosmic from 'cosmicjs';
const bucket = Cosmic().bucket({
slug: 'your_cosmic_slug',
read_key: 'your_cosmic_read_key',
});
export async function fetchDataByType(objectType = 'categories') {
const params = {
query: {
type: objectType,
},
props: 'title,slug,id,metadata',
sort: '-created_at',
};
try {
const data = await bucket.getObjects(params);
return data.objects;
} catch (error) {
return { error };
}
}
Cosmic also provide powerful content modeling features that let you create any kind of content super-fast and multi-channel publishing, for realizing create once and publish everywhere.
Data fetching approaches
Fetch-on-render
Fetch-on-render approach the network request is triggered in the component itself after mounting, the request isn’t triggered until the component renders. If you don't write a cleanup function that ignores stale responses, you'll notice a race condition (in React) bug when two slightly different requests for data have been made, and the application displays a different result depending on which request completes first. In fact on React 18, if you enable StrictMode in your application, in development mode you will find out that using useEffect will be invoked twice, because now React will mount your component, dismount, and then mount it again, to check if your code is working properly.
Let's fix a data fetching race condition by taking advantage of the useEffect
clean-up function. If we're okay with making several requests, but only rendering the last result, we can use a boolean flag isMount
:
// FetchWithUseEffect/App.js
import React, { useEffect, useState } from 'react';
import Category from './components/Category';
import { fetchDataByType } from './cosmic.js';
const App = () => {
const [categories, setCategories] = useState([]);
const getCategories = async () => {
const result = await fetchDataByType('categories');
if (result.length) {
setCategories(result);
}
};
useEffect(() => {
let isMount = true;
if (isMount) {
getCategories();
}
//useEffect clean-up function
return () => {
isMount = false;
};
}, []);
return (
<div className={cn('container', styles.container)}>
<div className={styles.sidebar}>
<div className={styles.collections}>
{categories?.map((category) => (
<Category key={category.id} info={category} />
))}
</div>
</div>
</div>
);
};
export default App;
Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect.
In this case, we still have a race condition in the sense that multiple requests to Cosmic will be in-flight, but only the results from the last one will be used.
Also as Dan Abramov explains, Fetch-on-render provides slow navigation between screens. If you have parent and child components both doing fetching in useEffects
, then the child component can't even start fetching until the parent component finishes fetching. These types of performance problems are very common in single-page apps and cause a lot more slowness than "excessive re-rendering" and if we have a complex application with multiple parallel requests, we would see different parts of the application load in random order. The more natural behavior for an application is to render things from top to bottom.
Render-as-you-fetch
Render-as-you-fetch approach lets us begin rendering our component immediately after triggering the network request and we start rendering pretty much immediately after kicking off the network request.
Suspense for Data Fetching
With Suspense, we don’t wait for the response to come back before we start rendering and reduce the Total Blocking Time (TBT) of our example from 106ms to 56ms.
The React core team set of concurrent features to make data fetching in React easier. Suspense is among these, and it aims to simplify managing loading states in React components. It’s a feature for managing asynchronous operations in a React app and lets you use <Suspense>
to declaratively “wait” for anything else, including data, and no longer have to wait for it all the JavaScript to load to start hydrating parts of the page.
First, we trigger the network request before rendering any components on line one. In the main App
component, we wrap both Category
and Cards
, Main
components in separate Suspense
components with their fallbacks.
When App
mounts for the first time, it tries to render Category
, which triggers the resourseCategories.read()
line. If the data isn’t ready yet (i.e., the request hasn’t been resolved), it is communicated back to Suspense, which then renders <p>Loading…</p>
. The same thing happens for Cards
and Main
// App.js
import React, { Suspense } from 'react';
const App = () => {
return (
<main>
<Suspense fallback={<p>Loading.....</p>}>
<Cards />
</Suspense>
<div>
<Suspense fallback={<p>Loading.....</p>}>
<Category />
</Suspense>
</div>
</main>
);
};
export default App;
Suspense
it is not a new interface to fetch data, as that job is still delegated to libraries like fetch or Axios, and Suspense
real job is to just say "show this code while is loading, and show that when it's done", nothing more than that.
Wrap your fetching logic wrapPromise.js
We also need wrap fetching logic, to throw an exception when our components are loading the data or it failed, but then simply return the response once the Promise
is resolved successfully and if it is still pending, it throws back the Promise.
// wrapPromise.js
//Wraps a promise so it can be used with React Suspense
function wrapPromise(promise) {
let status = 'pending';
let response;
const suspender = promise.then(
res => {
status = 'success';
response = res.objects;
},
err => {
status = 'error';
response = err;
},
);
const handler = {
pending: () => {
throw suspender;
},
error: () => {
throw response;
},
default: () => response,
};
const read = () => {
const result = handler[status] ? handler[status]() :
handler.default();
return result;
};
return { read };
}
export default wrapPromise;
At the end of the wrapPromise
function will check our promise's state, then return an object containing the read
function as a method, and this is what our React components will interact with to retrieve the value of the Promise.
Now we'll need to wrap the Cosmic call functions to wrapPromise
:
// cosmic.js
export function fetchDataByType(objectType = 'categories') {
const params = {
query: {
type: objectType,
},
props: 'title,slug,id,metadata',
sort: '-created_at',
};
const data = bucket.getObjects(params);
return wrapPromise(data);
}
The above is just an abstraction for Cosmic fetching functions with Suspense
and fetch one time.
Read the data in the component
Once everything is wrapped up on the fetching side of things, we want to use it in our component. So what is happening when we call the component, the read()
function will start to throw exceptions until it's fully resolved, and when that happens it will continue with the rest of the code, in our case to render it.
//components/Category
import React from 'react';
import { fetchDataByType } from '../../cosmic.js';
import styles from '../../styles/Collection.module.scss';
const resourseCategories = fetchDataByType();
const Category = () => {
const categories = resourseCategories.read();
const renderCategories = categories?.map((info) => (
<div key={info?.id} className={styles.user}>
<div className={styles.avatar}>
<img
className={styles.image}
src={info?.metadata?.image?.imgix_url}
alt="Avatar"
/>
</div>
<div className={styles.description}>
<div className={styles.name}>{info?.metadata?.title}</div>
<div
className={styles.money}
dangerouslySetInnerHTML={{ __html: info?.content }}
/>
</div>
</div>
));
return <div className={styles.collections}>{renderCategories}</div>;
};
export default Category;
The parent component
Suspense
gives React access to pending states in our applications and thats why React knows that a network call is happening, this allows us to render a fallback component declaratively while waiting.
// App.js
import React, { Suspense } from 'react';
import Cards from './components/Cards';
import Category from './components/Category';
import Main from './components/Main';
import styles from './styles/Collection.module.scss';
const App = () => {
return (
<div className={styles.wrapper}>
<div className={cn('section-pb', styles.section)}>
<div className={cn('container', styles.container)}>
<div className={styles.row}>
<Suspense fallback={<p>Loading.....</p>}>
<Main />
<Cards />
</Suspense>
</div>
<div className={styles.sidebar}>
<div className={styles.info}>
Collections
<span className={styles.smile} role="img" aria-label="fire">
🔥
</span>
</div>
<Suspense fallback={<p>Loading.....</p>}>
<Category />
</Suspense>
</div>
</div>
</div>
</div>
);
};
export default App;
Conclusion
Now, with Suspense
, you can break your app into small, stand-alone units that can be rendered on their own without the rest of the app, allowing content to be available to your user even much faster than before. We explored the various data fetching approaches for comparison.
Try it in your own project and give us your feedback. You can get started with Cosmic for a quick CMS to test data fetching with Suspense
for websites and apps.
Posted on August 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 15, 2023