Back to all notes

use() with Promises : Async Data with Suspense in React 19

Pass a promise to use() and let Suspense handle loading and rendering.

2 min read

Introduction

use() can also consume promises. When you pass a promise to use(), React suspends while it’s pending and resumes when it resolves, integrating async data with Suspense.

Why this matters

use() integrates async values with Suspense. When you pass a promise to use(), React will suspend while it’s pending and resume when it resolves—no manual state juggling.

The problem

Fetching inside components with useEffect and useState forces you to juggle loading/error state and handle race conditions.

Inefficient approach

Manual state orchestration is verbose and error‑prone:

import { useState, useEffect } from "react";

function fetchMessage() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Hello from the promise!"), 2000);
  });
}

function Message() {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [message, setMessage] = useState("");

  useEffect(() => {
    let cancelled = false;
    fetchMessage()
      .then((m) => !cancelled && setMessage(m))
      .catch((e) => !cancelled && setError(e))
      .finally(() => !cancelled && setLoading(false));
    return () => {
      cancelled = true;
    };
  }, []);

  if (loading) return <p>⌛ Loading message...</p>;
  if (error) return <p>Something went wrong</p>;
  return <p>Here is the message: {message}</p>;
}

function App() {
  return (
    <div>
      <h1>Use Hook with Promises</h1>
      <Message />
    </div>
  );
}

export default App;

The solution

Create a promise and read it with use() inside a Suspense boundary.

import { use, Suspense, useMemo } from "react";

function fetchMessage() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Hello from the promise!"), 2000);
  });
}

function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}

function App() {
  const messagePromise = useMemo(() => fetchMessage(), []);

  return (
    <div>
      <h1>Use Hook with Promises</h1>
      <Suspense fallback={<p>⌛ Loading message...</p>}>
        <Message messagePromise={messagePromise} />
      </Suspense>
    </div>
  );
}

export default App;

Step-by-step

  1. Create or obtain a promise (e.g., fetchMessage()).
  2. Read the promise with use(promise) inside a component.
  3. Wrap the component in <Suspense fallback={...}> to show loading UI.
  4. Keep the promise stable between renders (create it above render or memoize).
  5. Pair with an error boundary to handle rejections.

Tips

  • Keep promises stable between renders, or you’ll keep suspending.
  • Fetch at the route or loader level when possible; pass promises down.
  • Pair with error boundaries to handle rejected promises.

Knowledge base

Problem snippet:

useEffect(() => {
  /* fetch and set state */
}, []);

Solution snippet:

<Suspense fallback={<p>⌛ Loading...</p>}>
  <Message messagePromise={messagePromise} />
</Suspense>