| |

React 57 ⚛️ Reading Promises in Render with use Hook

⏱️ Estimated read: 17 min | |

A component that needs data from an asynchronous source has traditionally managed three states manually: loading, loaded, and error. The useEffect + useState pattern tracks each one, sets the loading flag, fires the fetch, updates the data, and clears the flag. React 19 introduces the use API, which collapses that pattern into a single read. A Promise is passed to use, and the component suspends until the Promise resolves, letting Suspense show the fallback and Error Boundaries catch rejections . The hook is not a replacement for every data-fetching pattern, but for the case where a Promise is already available and stable, it removes the entire state machine.

Key point: use reads a Promise that must be created outside the render. A Promise created inside a Client Component during render is recreated on every render, causing an infinite suspend loop. The Promise must come from a Server Component, a parent, a cache, or a memoized value.


Why the use API exists

The effect-state problem. The useEffect + useState pattern requires three pieces of state: the data, the loading flag, and the error. The effect runs after render, so the first render always shows a loading state. The pattern works, but it is verbose, and every component that fetches data repeats it.

The Suspense integration problem. Suspense boundaries show fallbacks while async work is pending. Without a way to read a Promise during render, a component cannot suspend on a fetch. It must manage its own loading state, which means the Suspense boundary never knows the component is waiting.

The conditional reading problem. useContext must be called at the top level of a component. The use API can be called conditionally, which lets a component read a context value only when it needs it . This is a secondary benefit, but it simplifies components that consume multiple contexts.

The boilerplate problem. A component that reads a resolved value should not need three state variables and an effect. The use API reads the value directly, and the surrounding Suspense and Error Boundary handle the pending and error states.

The server streaming problem. A Server Component can start a fetch and pass the Promise to a Client Component. The Client Component reads the Promise with use, and the data streams from server to client without blocking the initial render .

The stability problem. The Promise passed to use must be stable across renders. A Promise created inside a Client Component’s render is new on every render, and use suspends on it again, causing an infinite loop . The fix is to create the Promise in a Server Component, a cache, or a useMemo.


a. The use API

use is imported from react. It takes a resource, which can be a Promise or a context, and returns the resolved value .

The basic usage

import { use } from "react";

function Message({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

The messagePromise is passed as a prop. The component suspends until the Promise resolves, then renders the message. If the Promise rejects, the nearest Error Boundary catches the error .

The Suspense integration

The component that calls use must be wrapped in a <Suspense> boundary to show a fallback while the Promise is pending.

import { Suspense } from "react";

function App() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

The fallback is displayed until the Promise resolves. Once it resolves, the Message component renders with the data .

The conditional call

Unlike React Hooks, use can be called inside conditions and loops . This is the key difference from useContext, which must be called at the top level.

function Button({ showTheme }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <button className={theme.button}>Click</button>;
  }
  return <button>Click</button>;
}

The use call is inside an if, which would be a violation for any other hook .

The context reading

use can read a context value, replacing useContext in React 19 .

import { use } from "react";

function Button() {
  const theme = use(ThemeContext);
  return <button className={theme}>Click</button>;
}

The hook searches up the component tree for the nearest provider, exactly like useContext .

The Promise stability requirement

The Promise must be stable across renders. A Promise created inside the render is new every time, and use suspends on each new Promise, causing an infinite loop .

// ❌ WRONG: creates a new Promise on every render
function User({ userId }) {
  const user = use(fetch(`/api/users/${userId}`).then(r => r.json()));
  return <div>{user.name}</div>;
}

// ✅ CORRECT: memoize the Promise
function User({ userId }) {
  const userPromise = useMemo(() => fetchUser(userId), [userId]);
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

The useMemo ensures the same Promise is used across renders until userId changes .

The Server Component pattern

The recommended pattern is to create the Promise in a Server Component and pass it to a Client Component .

// Server Component
import { Suspense } from "react";
import { Message } from "./message";

export default function App() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}
// Client Component
"use client";
import { use } from "react";

export function Message({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

The Promise is created once on the server and remains stable on the client. The data streams to the client as the Promise resolves .


b. Common patterns

The use API serves a small set of patterns. Each one places the Promise creation in a stable scope and reads it in a Client Component.

The server-to-client streaming

The Server Component creates the Promise and passes it to the Client Component. The Client Component reads it with use. The data streams without blocking the initial HTML .

// Server
const dataPromise = fetchData();
<Suspense fallback={<Skeleton />}>
  <ClientComponent dataPromise={dataPromise} />
</Suspense>

// Client
"use client";
const data = use(dataPromise);

The cached Promise

A cache stores the Promise keyed by an identifier. The same Promise is reused across renders, avoiding the infinite loop .

const cache = new Map();

function getData(key) {
  if (!cache.has(key)) {
    cache.set(key, fetchData(key));
  }
  return cache.get(key);
}

function Component({ id }) {
  const data = use(getData(id));
  return <div>{data}</div>;
}

The context replacement

use replaces useContext and can be called conditionally .

function Component({ show }) {
  if (show) {
    const theme = use(ThemeContext);
    return <div className={theme}>...</div>;
  }
  return <div>...</div>;
}

The parallel data loading

Multiple Promises can be read in the same component. They resolve in parallel if they are created before the component renders .

function Dashboard({ userPromise, postsPromise }) {
  const user = use(userPromise);
  const posts = use(postsPromise);
  return <div>{user.name} has {posts.length} posts</div>;
}

The two Promises resolve concurrently. The Suspense boundary waits for both .

The waterfall avoidance

Sequential use calls create waterfalls: the second Promise is created only after the first resolves . To avoid this, create both Promises before the component renders.

// ❌ Waterfall
const user = use(fetchUser(userId));
const posts = use(fetchPosts(user.id));  // Waits for user

// ✅ Parallel
const userPromise = fetchUser(userId);
const postsPromise = fetchPosts(userId);  // Both start immediately
const user = use(userPromise);
const posts = use(postsPromise);

The error handling

A rejected Promise is thrown during render and caught by the nearest Error Boundary .

<ErrorBoundary fallback={<Error />}>
  <Suspense fallback={<Loading />}>
    <Component dataPromise={dataPromise} />
  </Suspense>
</ErrorBoundary>

The use hook does not catch errors. It throws them to the boundary .

The refetch pattern

To refresh data, a new Promise is created and stored in state inside a startTransition. React keeps showing the old content until the new Promise resolves .

function refetch() {
  startTransition(() => {
    setDataPromise(fetchData(id));  // New Promise
  });
}

The startTransition prevents the Suspense fallback from flashing during the refetch .


c. Caveats and limitations

use is a focused API with specific behavior. The caveats are what prevent the most common mistakes.

The Promise stability

The Promise must not be recreated on every render. A new Promise causes use to suspend again, and React re-renders, creating another new Promise, in an infinite loop . The fix is a cache, a useMemo, or a Promise created in a Server Component.

The try/catch restriction

use cannot be called inside a try/catch block . The error handling is done by Error Boundaries, not by try/catch.

// ❌ WRONG
try {
  const data = use(promise);
} catch (e) {
  // This does not work.
}

The component or hook requirement

use must be called inside a component or a hook. It cannot be called in a regular function .

The server component preference

In a Server Component, async and await are preferred over use. The await picks up rendering from the point where it was invoked, while use re-renders the component after the data resolves .

The serialization requirement

When a Promise is passed from a Server Component to a Client Component, its resolved value must be serializable. Functions and other non-serializable values cannot cross the boundary .

The conditional use caution

While use can be called conditionally, it should always be called for the Promise itself. Do not read promise.status or promise.value directly to bypass use. Always pass the Promise to use and let React handle it .


Complete Example Session

// ============================================
// PART 1: THE BASIC USE
// ============================================

import { use, Suspense } from "react";

function Message({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

function App({ messagePromise }) {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

// ============================================
// PART 2: THE SERVER COMPONENT PATTERN
// ============================================

// Server Component
async function fetchMessage() {
  await new Promise((r) => setTimeout(r, 1000));
  return "Hello from the server";
}

export default function ServerApp() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

// The Promise is created on the server and passed to the client.

// ============================================
// PART 3: THE CLIENT COMPONENT
// ============================================

"use client";
import { use } from "react";

export function Message({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

// The client component reads the Promise with use.
// Suspense shows the fallback while pending.

// ============================================
// PART 4: THE INFINITE LOOP PITFALL
// ============================================

// ❌ WRONG: creates a new Promise on every render
function BadUser({ userId }) {
  const user = use(fetch(`/api/users/${userId}`).then((r) => r.json()));
  return <div>{user.name}</div>;
}

// ✅ CORRECT: memoize the Promise
import { useMemo } from "react";

function GoodUser({ userId }) {
  const userPromise = useMemo(() => fetchUser(userId), [userId]);
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

// ============================================
// PART 5: THE CACHED PROMISE
// ============================================

const promiseCache = new Map();

function getCachedPromise(key) {
  if (!promiseCache.has(key)) {
    promiseCache.set(key, fetchData(key));
  }
  return promiseCache.get(key);
}

function CachedComponent({ id }) {
  const data = use(getCachedPromise(id));
  return <div>{data}</div>;
}

// ============================================
// PART 6: CONDITIONAL CONTEXT
// ============================================

function ThemedButton({ showTheme }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <button className={theme}>Themed</button>;
  }
  return <button>Default</button>;
}

// use can be called conditionally.
// useContext cannot.

// ============================================
// PART 7: PARALLEL LOADING
// ============================================

function Dashboard({ userPromise, postsPromise }) {
  const user = use(userPromise);
  const posts = use(postsPromise);
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{posts.length} posts</p>
    </div>
  );
}

// Both Promises are created before the component renders.
// They resolve in parallel.

// ============================================
// PART 8: ERROR HANDLING
// ============================================

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) {
      return <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}

function AppWithError() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<p>Loading...</p>}>
        <Message messagePromise={messagePromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

// A rejected Promise is caught by the Error Boundary.

// ============================================
// PART 9: THE REFETCH PATTERN
// ============================================

import { useState, startTransition } from "react";

function RefetchableComponent({ id }) {
  const [promise, setPromise] = useState(() => fetchData(id));

  function refetch() {
    startTransition(() => {
      setPromise(fetchData(id));
    });
  }

  const data = use(promise);

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      <p>{data}</p>
    </div>
  );
}

// startTransition keeps the old content visible during the refetch.

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// Server Component
export default function Page() {
  const dataPromise = fetchData();
  return (
    <ErrorBoundary>
      <Suspense fallback={<Skeleton />}>
        <DataDisplay dataPromise={dataPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

// Client Component
"use client";
function DataDisplay({ dataPromise }) {
  const data = use(dataPromise);
  return <div>{data}</div>;
}

// The Promise is created on the server, stable on the client.
// Suspense handles pending.
// ErrorBoundary handles rejection.
// use reads the resolved value.

The ten parts showed the basic usage, the server pattern, the client component, the infinite loop pitfall, the cached Promise, conditional context, parallel loading, error handling, the refetch pattern, and the complete pattern.


Quick Reference

use API

AspectValue
Importimport { use } from "react"
ParametersA Promise or a context
ReturnsThe resolved value
Conditional callsAllowed (unlike other hooks)
try/catchNot allowed
Error handlingError Boundaries

Promise Stability

ApproachStable?
Created in Server ComponentYes
Created in a cacheYes
Created with useMemoYes
Created in Client renderNo (infinite loop)

Common Patterns

PatternPromise Source
Server streamingServer Component
Cached dataExternal cache
Parallel loadingBoth created before render
RefetchNew Promise in startTransition

Suspense and Error Boundaries

StateHandled By
Pending<Suspense> fallback
ResolvedThe component renders
Rejected<ErrorBoundary>

use vs useEffect+useState

AspectuseuseEffect + useState
Read locationDuring renderAfter render
Loading stateSuspenseManual flag
Error stateError BoundaryManual state
BoilerplateMinimalVerbose
Promise stabilityRequiredNot applicable

Best Practices

✅ Do This:

// Create the Promise in a Server Component.
const dataPromise = fetchData();  // ✅

// Pass the Promise to a Client Component.
<ClientComponent dataPromise={dataPromise} />  // ✅

// Read the Promise with use.
const data = use(dataPromise);  // ✅

// Wrap in Suspense.
<Suspense fallback={<Skeleton />}><ClientComponent /></Suspense>  // ✅

// Use ErrorBoundary for rejections.
<ErrorBoundary><Suspense>...</Suspense></ErrorBoundary>  // ✅

// Memoize the Promise in Client Components.
const promise = useMemo(() => fetchData(id), [id]);  // ✅

// Use startTransition for refetch.
startTransition(() => setPromise(fetchData(id)));  // ✅

❌ Don’t Do This:

// Don't create the Promise in a Client Component render.
const data = use(fetch(url).then((r) => r.json()));  // ❌ Infinite loop

// Don't call use in try/catch.
try { const data = use(promise); } catch {}  // ❌ Not supported

// Don't read promise.status directly.
if (promise.status === "fulfilled") return promise.value;  // ❌

// Don't call use outside a component or hook.
use(promise);  // ❌ In a regular function

// Don't forget Suspense.
<Component dataPromise={promise} />  // ❌ No fallback

// Don't forget ErrorBoundary.
// A rejected Promise throws during render.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Infinite suspend loopPromise recreated on every renderUse useMemo, cache, or Server Component
No fallback shownMissing <Suspense>Wrap the component
Uncaught errorMissing Error BoundaryAdd an Error Boundary
use in try/catchNot supportedMove to Error Boundary
WaterfallSequential use callsCreate Promises before render
Serialization errorNon-serializable Promise valueUse serializable data
Conditional use on promise itselfBypassing useAlways pass the Promise to use

Real-World Examples

1. Basic Use

const message = use(messagePromise);

2. Server Component

const dataPromise = fetchData();
<Suspense fallback={<Skeleton />}><Client dataPromise={dataPromise} /></Suspense>

3. Client Component

"use client";
const data = use(dataPromise);

4. Memoized Promise

const promise = useMemo(() => fetchData(id), [id]);
const data = use(promise);

5. Cached Promise

const data = use(getCachedPromise(id));

6. Conditional Context

if (show) { const theme = use(ThemeContext); }

7. Parallel Loading

const user = use(userPromise);
const posts = use(postsPromise);

8. Error Boundary

<ErrorBoundary><Suspense>...</Suspense></ErrorBoundary>

9. Refetch

startTransition(() => setPromise(fetchData(id)));

10. Complete Pattern

// Server creates Promise, Client reads with use, Suspense and ErrorBoundary wrap.

Visual

The use Flow

┌────────────────────────────────────────────────────────────┐
│  use FLOW                                                  │
│                                                            │
│  SERVER COMPONENT                                          │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const dataPromise = fetchData();                  │     │
│  │  <Suspense fallback={<Skeleton />}>                │     │
│  │    <ClientComponent dataPromise={dataPromise} />   │     │
│  │  </Suspense>                                       │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           │  Promise passed as prop       │
│                           v                                │
│  CLIENT COMPONENT                                          │
│  ┌──────────────────────────────────────────────────┐     │
│  │  "use client";                                    │     │
│  │  const data = use(dataPromise);                    │     │
│  │  return <div>{data}</div>;                         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  PENDING → Suspense shows fallback                         │
│  RESOLVED → Component renders with data                    │
│  REJECTED → Error Boundary catches                         │
└────────────────────────────────────────────────────────────┘

The Infinite Loop

┌────────────────────────────────────────────────────────────┐
│  THE INFINITE LOOP PITFALL                                 │
│                                                            │
│  ❌ WRONG                                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  function Component() {                           │     │
│  │    const data = use(fetch(url).then(r => r.json()));│    │
│  │    return <div>{data}</div>;                      │     │
│  │  }                                                │     │
│  │                                                   │     │
│  │  Render 1: creates Promise A, suspends.           │     │
│  │  Promise A resolves, re-render.                   │     │
│  │  Render 2: creates Promise B, suspends.           │     │
│  │  Promise B resolves, re-render.                   │     │
│  │  Render 3: creates Promise C, suspends.           │     │
│  │  ... infinite loop.                               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  ✅ CORRECT                                                 │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const promise = useMemo(() => fetchData(id), [id]);│    │
│  │  const data = use(promise);                       │     │
│  │                                                   │     │
│  │  Render 1: creates Promise A, suspends.           │     │
│  │  Promise A resolves, re-render.                   │     │
│  │  Render 2: same Promise A (memoized), renders.    │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

use vs useContext

┌────────────────────────────────────────────────────────────┐
│  use vs useContext                                         │
│                                                            │
│  useContext                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Must be called at the top level.                 │     │
│  │  Cannot be conditional.                           │     │
│  │  Cannot be in a loop.                             │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  use                                                       │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Can be called conditionally.                     │     │
│  │  Can be called in loops.                          │     │
│  │  Can read Promises as well as context.            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  use is preferred in React 19.                             │
└────────────────────────────────────────────────────────────┘

Suspense and Error Boundaries

┌────────────────────────────────────────────────────────────┐
│  SUSPENSE AND ERROR BOUNDARIES                             │
│                                                            │
│  <ErrorBoundary fallback={<Error />}>                      │
│    <Suspense fallback={<Loading />}>                       │
│      <Component dataPromise={promise} />                   │
│    </Suspense>                                             │
│  </ErrorBoundary>                                          │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  PENDING → Suspense shows <Loading />             │     │
│  │  RESOLVED → Component renders                     │     │
│  │  REJECTED → ErrorBoundary shows <Error />         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The two boundaries handle the two failure modes.          │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
useReact API that reads a Promise or context
Importimport { use } from "react"
Conditional callsAllowed (unlike other hooks)
try/catchNot allowed
Promise stabilityRequired across renders
Server ComponentPreferred source of the Promise
SuspenseHandles pending state
Error BoundaryHandles rejection
Infinite loopCaused by Promise recreated on every render
CachingRequired for Client-side Promises

Key takeaways:

  • use reads a Promise during render. The component suspends until the Promise resolves, letting Suspense show the fallback and Error Boundaries catch rejections. The useEffect + useState pattern is replaced by a single read.
  • The Promise must be stable across renders. A Promise created inside a Client Component’s render is new every time, causing an infinite suspend loop. The fix is to create the Promise in a Server Component, a cache, or a useMemo.
  • use can be called conditionally. Unlike other React hooks, use can be called inside if statements and loops. This makes it more flexible than useContext for conditional context reading.
  • The Server Component pattern is preferred. The Server Component creates the Promise and passes it to the Client Component. The Client Component reads it with use. The data streams without blocking the initial render.
  • use cannot be called in try/catch. Error handling is done by Error Boundaries. The use hook throws the rejection to the nearest boundary.
  • Parallel loading requires creating Promises before render. Sequential use calls create waterfalls. Creating all the Promises before the component renders lets them resolve in parallel.
  • The refetch pattern uses startTransition. A new Promise is created and stored in state inside a startTransition, which keeps the old content visible until the new Promise resolves.
  • use replaces useContext in React 19. The new API reads context values and can be called conditionally, which useContext cannot.

Remember: The use API is the React 19 primitive for reading a Promise or context during render. It suspends the component until the Promise resolves, integrates with Suspense for the pending state and Error Boundaries for the rejected state, and can be called conditionally. The Promise must be stable across renders; a Promise created inside a Client Component’s render causes an infinite loop. The preferred pattern is to create the Promise in a Server Component, pass it to a Client Component, and read it with use. The useEffect + useState fetch dance is replaced by a single read, and the loading and error states are handled by the boundaries that wrap the component. The result is a declarative, boilerplate-free way to read asynchronous data in React.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!