How to Use React Suspense for Data Fetching (Best Practices)

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

  1. Extra Boilerplate – Developers must manually track loading and error states.
  2. Extra Re-renders – The component renders first without data, then again after fetching.
  3. 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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *