React 19HooksAPI DesignCleaner CodeArchitecture

Mastering the use() Hook: One API to Rule Them All 🔗

R
React Core Team
Featured Guide 40 min read

Stop writing useEffect.

For a decade, we have suffered in "Hook Hell." Accessing asynchronous data or global context meant abiding by the strict "Rules of Hooks." Top level only. No loops. No conditions.

React 19 breaks these chains with the universal use() API. It isn't just a hook; it's a Control Flow Primitive. It unwraps promises. It consumes context. And for the first time in React history: It runs inside loops and if-statements.

The use API represents the convergence of React's Server and Client stories. It is the bridge that allows data to flow from your backend (Promises) directly into your UI (Components) without the boilerplate of useState, useEffect, and manual loading states.

This guide covers everything from the basic syntax to advanced architectural patterns that are only possible with this new primitive.

02. Data Fetching: Unwrapping Promises

In React 18 and earlier, handling a promise meant managing three states: loading, error, and success. With use(), you simply "ask" for the value. If the promise is pending, React Suspends the component. If it rejects, React hits the Error Boundary. If it resolves, execution continues.

The Old Way (Imperative)

function UserProfile({ id }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let active = true;
    setIsLoading(true);
    fetchUser(id).then(u => {
      if (active) {
        setUser(u);
        setIsLoading(false);
      }
    });
    return () => { active = false; }
  }, [id]);

  if (isLoading) return <Spinner />;
  return <div>{user.name}</div>;
}
                    

The New Way (Declarative)

import { use } from 'react';

// The promise is passed IN. The component reads it.
// Loading is handled by <Suspense> in the parent.
function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <div>{user.name}</div>;
}
                    

How it works under the hood

When use(promise) is called:

  • If the promise is Pending: React throws the promise (literally throws it like an error). The nearest <Suspense> boundary catches it and renders the fallback.
  • When the promise Resolves: React re-renders the component. This time, use(promise) returns the resolved value immediately.
  • If the promise Rejects: React throws the error, which bubbles up to the nearest Error Boundary.

Deep Dive: Error Boundaries

Because rejecting promises are thrown as errors, Error Boundaries are no longer optional.

If you don't wrap a component that uses `use(promise)` in an Error Boundary, a single network failure will crash your entire application (White Screen of Death).

03. The Context Revolution

useContext is one of the most used hooks, but it has a major flaw: it must be called at the top level. This means you often subscribe to contexts that you might not even use, just in case.

With use(Context), you can read context only when you need it.

import { use } from 'react';
import { ThemeContext } from './theme';

function Header({ showSettings }) {
  // 1. Standard Render
  if (!showSettings) {
    return <div>Welcome</div>;
  }

  // 2. Conditional Context Subscription
  // This line only runs if showSettings is true.
  // We save the cost of subscription when it's false.
  const theme = use(ThemeContext);
  
  return <div style={{ color: theme.color }}>Settings</div>;
}

Performance Win: In large apps, avoiding unnecessary context subscriptions can significantly reduce render checking overhead.

04. Conditional Hooks? Yes.

This breaks the Golden Rule of hooks: "Don't call hooks inside loops, conditions, or nested functions."

The use API is an exception. It is technically a function, not a "Hook" in the traditional sense (it doesn't maintain fiber state order in the same rigid way for storage, as it relies on the promise/context identity).

Loops

function UserList({ users }) {
  return (
    <ul>
      {users.map(user => {
        // ✅ Valid!
        const details = use(user.detailsPromise);
        return <li>{details.bio}</li>;
      })}
    </ul>
  );
}
                     

Early Returns

function Panel({ isExpanded }) {
  if (!isExpanded) return null;

  // ✅ Valid! Runs after an early return.
  const data = use(somePromise);
  return <div>{data}</div>;
}
                     

05. Critical Pitfalls

Pitfall 1: Creating Promises during Render

Do NOT create a promise inside the component function body.

// ❌ WRONG - Infinite Loop Hazard
function Component() {
  const promise = fetch('/api'); // Creates a NEW promise every render!
  const data = use(promise);     // Suspends. Re-renders. New Promise. Suspends...
}

// ✅ RIGHT
// Pass promise as prop, or create in a Server Component / External Cache
function Component({ dataPromise }) {
  const data = use(dataPromise);
}
                    

Pitfall 2: 'use' is for Reading, not Fetching

use() is designed to read a value that is already being fetched. It shouldn't trigger the fetch itself. The fetch should be initiated by:

  • A Server Component (preferred)
  • A library like TanStack Query
  • An event handler

06. Migration Strategy 2026

You don't need to rewrite your entire app. Adopt use() iteratively.

  1. Start with Leaf Components: Convert small components that currently receive data props to instead receive Promises of data.
  2. Hoist Data Fetching: Move your fetch calls up to the parent or Server Component. Pass the promise down.
  3. Replace Context Hooks: Identify components that conditionally render. Swap useContext for use(Context) and move the call inside the conditional block.

07. Try the API

The component below demonstrates use() mimicking a Suspense falback.

Interactive Playground

import React, { useState, Suspense } from 'react';

// 🔮 MOCKING THE USE HOOK BEHAVIOR 
// NOTE: Since 'use' is a React 19 feature, this demo simulates 
// the behavior (Suspense + fetching) using standard patterns available in this environment,
// but presenting the Mental Model of how 'use' works.

// Valid Promise cache
const cache = new Map();

function fetchData(id) {
  if (!cache.has(id)) {
    cache.set(id, new Promise(resolve => {
      setTimeout(() => {
        resolve({
          id,
          title: `Dashboard Data for ID: ${id}`,
          stats: Math.floor(Math.random() * 10000),
          status: 'Active',
          lastUpdated: new Date().toLocaleTimeString()
        });
      }, 2000); // 2s delay
    }));
  }
  return cache.get(id);
}

// --------------------------------------------------------
// IN REACT 19, THIS COMPONENT WOULD LOOK LIKE THIS:
// --------------------------------------------------------
// function DataCard({ dataPromise }) {
//   const data = use(dataPromise); 
//   return <div>{data.title}</div>
// }
// --------------------------------------------------------

// Simulation wrapper
function SimulatedDataView({ id }) {
   const [data, setData] = useState(null);
   const [loading, setLoading] = useState(false);

   // simulating the "use" unwrap effect visually
   React.useEffect(() => {
       setLoading(true);
       fetchData(id).then(res => {
           setData(res);
           setLoading(false);
       })
   }, [id]);

   if (loading || !data) {
       // This mimics what Suspense does while 'use' waits
       return (
           <div className="h-48 bg-gray-100 dark:bg-slate-800 rounded-xl animate-pulse flex items-center justify-center border border-gray-200 dark:border-slate-700">
               <span className="w-8 h-8 text-4xl animate-spin">🌀</span>
               <span className="ml-2 text-gray-400 font-bold">Suspending...</span>
           </div>
       )
   }

   return (
       <div className="h-48 bg-white dark:bg-slate-900 border border-cyan-100 dark:border-cyan-900/30 rounded-xl p-6 shadow-sm flex flex-col justify-center animate-in fade-in zoom-in-95 duration-300 relative overflow-hidden group">
           <div className="absolute top-0 left-0 w-1 h-full bg-cyan-500 transform scale-y-0 group-hover:scale-y-100 transition-transform"></div>
           <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{data.title}</h3>
           <div className="text-4xl font-black text-cyan-600 dark:text-cyan-400 tabular-nums mb-2">{data.stats}</div>
           <div className="flex justify-between items-center text-xs uppercase font-bold text-gray-400 tracking-wider">
               <span>Total Visitors</span>
               <span>{data.lastUpdated}</span>
           </div>
       </div>
   )
}

export default function UseHookDemo() {
  const [queryId, setQueryId] = useState(1);
  const [showSecond, setShowSecond] = useState(false);

  return (
    <div className="bg-slate-50 dark:bg-black/20 p-8 rounded-3xl border border-gray-200 dark:border-white/10 shadow-lg">
        <div className="flex flex-col md:flex-row justify-between items-center mb-8 gap-4">
            <div>
                 <h3 className="text-xl font-bold text-gray-900 dark:text-white">Dashboard (Suspense Architecture)</h3>
                 <p className="text-sm text-gray-500">Simulating the "Render-as-you-fetch" pattern</p>
            </div>
            
            <div className="flex gap-2">
                <button 
                  onClick={() => setShowSecond(!showSecond)} 
                  className="bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-lg font-bold hover:bg-gray-50 dark:hover:bg-slate-700 transition"
                >
                    {showSecond ? 'Hide Extra' : 'Show Extra'}
                </button>
                <button 
                  onClick={() => setQueryId(q => q+1)} 
                  className="bg-cyan-600 hover:bg-cyan-500 text-white px-4 py-2 rounded-lg font-bold transition-colors shadow-cyan-900/20 shadow-lg"
                >
                    Load New Data
                </button>
            </div>
        </div>
        
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
             {/* Imagine these are wrapped in <Suspense> */}
             <SimulatedDataView id={queryId} />
             
             {showSecond ? (
                 <SimulatedDataView id={queryId + 100} />
             ) : (
                <div className="h-48 border-2 border-dashed border-gray-200 dark:border-slate-800 rounded-xl flex items-center justify-center text-gray-400 font-bold">
                    Conditional Slot
                </div>
             )}
        </div>
        
        <div className="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 text-sm rounded-lg border border-yellow-200 dark:border-yellow-900/30">
            <strong>Architecture Check:</strong> Notice how the data fetching logic is decoupled from the rendering logic in the mental model. The component just 'uses' the data. It assumes it's there. React handles the waiting.
        </div>
    </div>
  );
}