Published: 3 March 2023
Introduction
For years, React developers have relied on useEffect to handle data fetching. While useEffect works, it has limitations:
- It triggers requests after the component renders, causing an extra re-render.
- Managing loading states, errors, and caching requires extra boilerplate.
- Handling race conditions and optimizing performance is developer-dependent.
With React Suspense, data fetching becomes more declarative and efficient. Instead of manually handling loading and error states, Suspense lets React pause rendering until data is available.
What This Guide Covers
- Why Suspense is better than
useEffect
for data fetching. - How to use Suspense with Next.js, React Query, and Fetch API.
- Best practices for integrating Suspense into your projects.
1. Why Suspense is Better Than useEffect for Data Fetching
The Problem with useEffect
A common approach to data fetching in React is using useEffect
with useState
:
import { useState, useEffect } from "react";
const Users = () => {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching data</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Problems with This Approach
- Extra Boilerplate – Developers must manually track
loading
anderror
states. - Extra Re-renders – The component renders first without data, then again after fetching.
- No Built-in Caching – Fetching happens every time the component renders, unless extra caching logic is added.
2. How Suspense Fixes These Issues
How Suspense Works
React Suspense pauses rendering until the requested data is available. Instead of handling loading and error states manually, Suspense allows React to suspend rendering and show a fallback UI until data arrives.
This makes data fetching more streamlined, readable, and efficient.
3. Using React Suspense for Data Fetching
Step 1: Create a Data Fetching Function
To integrate Suspense, you need a function that suspends execution while data is being fetched. A common pattern is to use a resource wrapper:
const fetchData = (url) => {
let status = "pending";
let result;
const promise = fetch(url)
.then((res) => res.json())
.then((data) => {
status = "success";
result = data;
})
.catch((err) => {
status = "error";
result = err;
});
return {
read() {
if (status === "pending") throw promise;
if (status === "error") throw result;
return result;
},
};
};
const userResource = fetchData("https://jsonplaceholder.typicode.com/users");
Step 2: Wrap Your Component in Suspense
Now, we use the read() method inside a component wrapped in Suspense
:
import { Suspense } from "react";
const UserList = () => {
const users = userResource.read();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const App = () => {
return (
<Suspense fallback={<p>Loading users...</p>}>
<UserList />
</Suspense>
);
};
export default App;
Why This is Better
✅ No Manual Loading/Error States – React automatically suspends rendering.
✅ No Extra Re-renders – The component only renders once when data is ready.
✅ Better Performance – Suspense enables features like streaming and automatic caching.
4. Using Suspense with React Query
If you’re using React Query, integrating Suspense is even easier. React Query provides built-in support for Suspense, eliminating the need for custom wrappers.
Step 1: Enable Suspense in React Query
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // Enable Suspense
},
},
});
Step 2: Use Suspense with React Query
import { useQuery } from "react-query";
import { Suspense } from "react";
const fetchUsers = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
};
const Users = () => {
const { data: users } = useQuery("users", fetchUsers);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<p>Loading users...</p>}>
<Users />
</Suspense>
</QueryClientProvider>
);
};
export default App;
Why This is Better Than useEffect
✅ Automatic Caching – React Query caches data, avoiding unnecessary re-fetches.
✅ Error Handling Built-in – Errors are automatically managed.
✅ Optimized Performance – Suspense ensures rendering is paused until data is ready.
5. Using Suspense in Next.js 13/14 with React Server Components
Next.js App Router (introduced in Next.js 13/14) fully embraces Suspense with React Server Components (RSC).
Fetching Data with Suspense in a Server Component
// app/users/page.tsx (Server Component)
import { Suspense } from "react";
const fetchUsers = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
};
const Users = async () => {
const users = await fetchUsers();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default function Page() {
return (
<Suspense fallback={<p>Loading users...</p>}>
<Users />
</Suspense>
);
}
In Next.js, Suspense delays rendering until the data is available, preventing empty UI flashes.
Final Thoughts
Suspense has transformed how React applications handle data fetching. Instead of manually managing loading and error states with useEffect
, Suspense pauses rendering until data is ready, leading to:
- Faster, more efficient rendering.
- Cleaner, more readable code.
- Built-in performance optimizations with caching and automatic handling.
If you’re building a modern React app, switching to Suspense (especially with React Query or Next.js) will greatly improve performance and developer experience.