The use() Hook & Suspense in Practice

Read promises and context with use(), and let Suspense own your loading UI declaratively.

React 19's use reads a resource during render — a promise or a context. When you hand it a promise, the component suspends until that promise settles, and the nearest <Suspense> boundary shows its fallback in the meantime. The result: no loading boolean, no useEffect fetch dance, no manual state machine for the happy path.

What is different about use()

  • It can be called conditionally — inside an if or a loop — unlike every other hook.
  • It participates in Suspense (for promises) and reads context (as a flexible alternative to useContext).

The one rule that trips everyone up

Do not create the promise inline in the component body:

// WRONG: a brand new promise every render -> infinite suspense loop
function Profile() {
  const user = use(fetch('/api/me').then(r => r.json()));
  return <p>{user.name}</p>;
}

The promise must be stable across renders — created by a parent, a cache, or a framework loader — and passed down as a prop. Otherwise each render suspends on a fresh promise and never resolves.

Suspense + Error Boundary = the full story

Suspense handles pending; it does not handle rejected. Wrap the boundary in an Error Boundary so a failed fetch shows a real error UI instead of an endless spinner. Together they give you three states — loading, error, success — declaratively, with the success path written as if the data were always there.

Example

Example · javascript
import { use, Suspense, useState } from 'react';

// Cache promises by key so the SAME promise is reused across renders.
const cache = new Map();
function fetchUser(id) {
  if (!cache.has(id)) {
    cache.set(
      id,
      fetch(`/api/users/${id}`).then((r) => {
        if (!r.ok) throw new Error('User not found');
        return r.json();
      })
    );
  }
  return cache.get(id);
}

function UserName({ id }) {
  const user = use(fetchUser(id)); // suspends until resolved
  return <strong>{user.name}</strong>;
}

function Profile() {
  const [id, setId] = useState(1);
  return (
    <div>
      <button onClick={() => setId((n) => n + 1)}>Next user</button>
      {/* Fallback shows while the stable promise is pending */}
      <Suspense fallback={<em>Loading user...</em>}>
        <UserName id={id} />
      </Suspense>
    </div>
  );
}

When to use it

  • A React 19 server component passes a data promise to a client component that calls use() so the component suspends automatically instead of managing a loading boolean.
  • A parallel data-loading pattern creates multiple promises simultaneously and passes each to a separate use() call so all requests fire at once and the page renders as each resolves.
  • A feature flag system wraps its config fetch in a promise that is cached at the module level and shared via use() so all components read the same resolved value without multiple fetches.

More examples

use() suspending on a promise

Passes a fetch promise to use() inside a Suspense boundary; the component suspends automatically until the data arrives.

Example · jsx
import { use, Suspense } from 'react';

function ArticleContent({ promise }) {
  const article = use(promise); // suspends until resolved
  return <article><h1>{article.title}</h1><p>{article.body}</p></article>;
}

function ArticlePage({ id }) {
  const promise = fetch(`/api/articles/${id}`).then(r => r.json());
  return (
    <Suspense fallback={<div className="skeleton" />}>
      <ArticleContent promise={promise} />
    </Suspense>
  );
}

Parallel data loading

Creates both promises before entering Suspense so they run in parallel, not sequentially.

Example · jsx
import { use, Suspense } from 'react';

function Dashboard({ userId }) {
  // Both fetches start simultaneously
  const userPromise = fetchUser(userId);
  const statsPromise = fetchStats(userId);

  return (
    <Suspense fallback={<p>Loading dashboard...</p>}>
      <UserHeader promise={userPromise} />
      <StatsPanel promise={statsPromise} />
    </Suspense>
  );
}

function UserHeader({ promise }) {
  const user = use(promise);
  return <h1>{user.name}</h1>;
}

function StatsPanel({ promise }) {
  const stats = use(promise);
  return <p>Posts: {stats.postCount}</p>;
}

use() with context conditionally

Calls use() inside a conditional branch — something useContext cannot do — without violating rules of hooks.

Example · jsx
import { use, createContext } from 'react';

const FeatureCtx = createContext({ darkMode: false });

function ThemeIcon({ show }) {
  // use() CAN be called inside a condition — unlike useContext
  if (!show) return null;
  const { darkMode } = use(FeatureCtx);
  return <span>{darkMode ? '🌙' : '☀️'}</span>;
}

Discussion

  • Be the first to comment on this lesson.