10LOC
Reactintermediate

Use React's use hook for promises and context

Published July 6, 2027

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

const ThemeContext = createContext("light");
const greetingCache = new Map<string, Promise<string>>();

const getGreeting = (name: string) => {
  if (!greetingCache.has(name)) {
    greetingCache.set(name, fetch(`/api/greet/${name}`).then((res) => res.text()));
  }
  return greetingCache.get(name)!;
};

const Greeting = ({ name }: { name: string }) => {
  const message = use(getGreeting(name));
  const theme = use(ThemeContext);
  return <p className={theme}>{message}</p>;
};

export const GreetingPage = ({ name }: { name: string }) => (
  <Suspense fallback={<p>Loading…</p>}>
    <Greeting name={name} />
  </Suspense>
);

What

use(promise) suspends the calling component until the promise resolves and returns its resolved value. use(context) reads a Context value, the same value useContext would return. GreetingPage below wires both into one render: Greeting reads a cached promise and a theme context in the same component.

Why it matters

Before use, reading a promise during render wasn't possible at all — data fetching meant useEffect + useState (fetch after mount, an extra render before data shows) or a library that hid its own Suspense wiring behind a hook. use(promise) lets a component suspend exactly where it reads the data, integrating with a wrapping <Suspense> the same way React.lazy already did for component code.

use(context) is the more surprising half. React's own docs are explicit that use is not a Hook — it doesn't follow the rules of hooks, and can be called conditionally, inside loops, or after an early return, which useContext can never do. That makes it useful in places useContext structurally can't reach, like a context read that's only relevant inside one branch of an if.

How it works

  • greetingCache / getGreeting memoize the promise per name, so the same Promise instance is returned across re-renders. This isn't optional: passing a freshly created promise to use on every render (use(fetch(...)) inline) means each render suspends on a promise that never has a chance to resolve before the next render starts a new one.
  • Greeting calls use(getGreeting(name)) first — this is where the component actually suspends — then use(ThemeContext), reading context the same render.
  • GreetingPage wraps Greeting in <Suspense fallback={...}>. A production version would also wrap it in an Error Boundary, since a rejected promise propagates there, not to a try/catch around use.

Gotchas

  • Never construct the promise inline inside use(...) during render — it has to come from somewhere stable (a cache, a prop, a ref), or the component suspends forever, re-fetching on every attempt.
  • use(promise) can't be wrapped in try/catch. A rejected promise is handled by the nearest Error Boundary, exactly like a thrown error during render.
  • "Not a Hook" only means use is exempt from the conditional-call restriction — it can still only be called during the render of a Component or another Hook, never inside an event handler or a useEffect callback.

Related

  • useSyncExternalStore — for state that lives outside React and changes over time via subscription; use(promise) is for a single resolution, not an ongoing stream of updates.
  • React.lazy — the original special case of suspending on a promise, scoped to loading a component definition; use generalizes the same mechanism to any promise.