Expert65 min read

React 19: The use Hook - Reading Promises and Context

Master the use hook to handle promises and context values directly in render functions with Suspense support.

Topics Covered:

use HookPromisesSuspenseContextAsync Rendering

Prerequisites:

  • React 19: Server Actions and useActionState

Overview

The use hook is React 19's powerful new hook for reading promises and context values. It enables direct promise handling in components, automatically suspending until promises resolve. This simplifies data fetching patterns and works seamlessly with Suspense boundaries. This tutorial covers promise handling, context reading, error boundaries, and integration with data fetching libraries.

Lesson 1: Understanding the use Hook

The use hook can read two types of resources: promises and context. What use Can Read: • Promises - automatically suspends until resolved • Context - reads context values (including conditional reading) Key Features: • Automatic Suspense integration • Promise unwrapping • Context reading (including conditional) • Error boundary integration When to Use: • Data fetching with promises • Reading context conditionally • Simplifying async component logic • Working with Suspense When NOT to Use: • Simple synchronous operations • Non-promise async operations (use useEffect) • Non-context values

Code Example:
// Basic promise reading
import { use } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // use unwraps the promise and suspends until it resolves
  const user = use(userPromise);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Usage with Suspense
function App() {
  const userPromise = fetchUser(userId);
  
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Context reading
import { createContext, use } from 'react';

const ThemeContext = createContext<'light' | 'dark' | undefined>(undefined);

function ThemedButton() {
  // Conditional context reading - won't error if context not provided
  const theme = use(ThemeContext);
  
  // theme is 'light' | 'dark' | undefined
  return (
    <button className={theme === 'dark' ? 'dark' : 'light'}>
      Themed Button
    </button>
  );
}

// use vs useContext
// useContext: Requires context to be provided, throws if not
// use: Can read conditionally, returns undefined if not provided

The use hook simplifies async rendering by unwrapping promises automatically and suspending until they resolve. It also enables conditional context reading.

Lesson 2: Reading Promises with use

The use hook's primary use case is reading promises in components. Promise Reading Pattern: 1. Pass promise as prop or create it 2. Call use(promise) in component 3. Component suspends until promise resolves 4. Render with resolved value Important Rules: • Promise should be stable (use useMemo or stable reference) • Wrap component in Suspense boundary • Handle errors with Error Boundaries • Use with data fetching libraries

Code Example:
// Basic promise reading
function DataComponent({ dataPromise }: { dataPromise: Promise<Data> }) {
  const data = use(dataPromise);
  
  return <div>{data.content}</div>;
}

// Stable promise reference
function PostList({ postIds }: { postIds: number[] }) {
  // Memoize promise to prevent re-creation
  const postsPromise = useMemo(
    () => fetchPosts(postIds),
    [postIds]
  );
  
  const posts = use(postsPromise);
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

// Multiple promises
function Dashboard({ 
  userPromise, 
  postsPromise 
}: { 
  userPromise: Promise<User>;
  postsPromise: Promise<Post[]>;
}) {
  const user = use(userPromise);
  const posts = use(postsPromise);
  
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <PostList posts={posts} />
    </div>
  );
}

// Error handling with Error Boundaries
function PostContent({ postPromise }: { postPromise: Promise<Post> }) {
  try {
    const post = use(postPromise);
    return <article>{post.content}</article>;
  } catch (error) {
    // Errors are thrown to nearest Error Boundary
    // Can also handle locally
    if (error instanceof Error) {
      return <div>Error: {error.message}</div>;
    }
    throw error; // Re-throw to Error Boundary
  }
}

// Integration with data fetching libraries
// React Query example
function useQueryData<T>(queryKey: string) {
  const queryClient = useQueryClient();
  const query = queryClient.getQueryState(queryKey);
  
  if (!query) {
    throw queryClient.fetchQuery(queryKey);
  }
  
  return use(query.dataPromise);
}

// SWR example
function useSWRData<T>(key: string) {
  const { data } = useSWR(key, fetcher);
  
  if (!data) {
    throw new Promise(resolve => {
      // Wait for SWR to resolve
      const unwatch = watch(() => {
        if (data) {
          unwatch();
          resolve(data);
        }
      });
    });
  }
  
  return data;
}

use automatically suspends components until promises resolve. Keep promises stable with useMemo, wrap in Suspense boundaries, and handle errors with Error Boundaries.

Lesson 3: Reading Context with use

use can also read context values, including conditional reading. Context Reading: • Similar to useContext • Can read conditionally • Returns undefined if context not provided • No error if context missing Use Cases: • Conditional context reading • Optional context values • Avoiding context provider requirements Comparison with useContext: • useContext: Required, throws if missing • use: Optional, returns undefined if missing

Code Example:
// Conditional context reading
import { createContext, use } from 'react';

const FeatureFlagsContext = createContext<Record<string, boolean> | undefined>(
  undefined
);

function FeatureComponent({ feature }: { feature: string }) {
  // Won't error if context not provided
  const flags = use(FeatureFlagsContext);
  
  if (!flags || !flags[feature]) {
    return null; // Feature disabled
  }
  
  return <div>Feature enabled!</div>;
}

// Usage - context is optional
function App() {
  // Works even without FeatureFlagsProvider
  return <FeatureComponent feature="newUI" />;
}

// With provider
function AppWithFlags() {
  return (
    <FeatureFlagsContext.Provider value={{ newUI: true }}>
      <FeatureComponent feature="newUI" />
    </FeatureFlagsContext.Provider>
  );
}

// Multiple context reading
const ThemeContext = createContext<'light' | 'dark' | undefined>(undefined);
const LanguageContext = createContext<'en' | 'es' | undefined>(undefined);

function ThemedComponent() {
  const theme = use(ThemeContext);
  const language = use(LanguageContext);
  
  // Both are optional
  const className = theme === 'dark' ? 'dark' : 'light';
  const text = language === 'es' ? 'Hola' : 'Hello';
  
  return <div className={className}>{text}</div>;
}

// vs useContext (required)
function RequiredContextComponent() {
  // Throws if context not provided
  const theme = useContext(ThemeContext); // Error if no provider!
  
  return <div className={theme}>Content</div>;
}

// use is safer for optional contexts
function OptionalContextComponent() {
  const theme = use(ThemeContext); // Safe, returns undefined if no provider
  
  return <div className={theme || 'light'}>Content</div>;
}

use enables optional context reading, returning undefined if context isn't provided. This is safer than useContext for optional features.

Lesson 4: Suspense Integration

use works seamlessly with Suspense boundaries. Suspense Behavior: • Component suspends when promise pending • Shows fallback from Suspense boundary • Resumes when promise resolves • Handles errors with Error Boundaries Best Practices: • Always wrap use(promise) in Suspense • Provide meaningful fallbacks • Use nested Suspense for granular loading • Handle errors properly

Code Example:
// Basic Suspense integration
function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <UserProfile userPromise={fetchUser()} />
    </Suspense>
  );
}

// Nested Suspense boundaries
function Dashboard() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <UserHeader userPromise={fetchUser()} />
      </Suspense>
      
      <Suspense fallback={<PostsSkeleton />}>
        <PostsList postsPromise={fetchPosts()} />
      </Suspense>
      
      <Suspense fallback={<StatsSkeleton />}>
        <Stats statsPromise={fetchStats()} />
      </Suspense>
    </div>
  );
}

// Error boundaries
function AppWithErrorHandling() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Suspense fallback={<Loading />}>
        <DataComponent dataPromise={fetchData()} />
      </Suspense>
    </ErrorBoundary>
  );
}

// Parallel data fetching
function ProfilePage({ userId }: { userId: number }) {
  // These promises can resolve independently
  const userPromise = fetchUser(userId);
  const postsPromise = fetchUserPosts(userId);
  const followersPromise = fetchFollowers(userId);
  
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userPromise={userPromise} />
      <Suspense fallback={<PostsSkeleton />}>
        <PostsList postsPromise={postsPromise} />
      </Suspense>
      <Suspense fallback={<FollowersSkeleton />}>
        <FollowersList followersPromise={followersPromise} />
      </Suspense>
    </Suspense>
  );
}

// Streaming with Suspense
function StreamingPage() {
  return (
    <div>
      {/* Renders immediately */}
      <Header />
      
      {/* Suspends, streams when ready */}
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent contentPromise={fetchContent()} />
      </Suspense>
      
      {/* Suspends, streams when ready */}
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar sidebarPromise={fetchSidebar()} />
      </Suspense>
    </div>
  );
}

Suspense boundaries control loading states for use hooks. Use nested boundaries for granular loading, and always include Error Boundaries for error handling.

Lesson 5: Real-World Patterns

Explore practical patterns using the use hook in production applications. Common Patterns: • Data fetching in components • Conditional data loading • Parallel data fetching • Error handling • Loading states • Integration with data libraries

Code Example:
// Real-world patterns

// 1. Server Components with use
// app/user/[id]/page.tsx (Next.js)
async function UserPage({ params }: { params: { id: string } }) {
  const userPromise = fetchUser(params.id);
  const postsPromise = fetchUserPosts(params.id);
  
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsList postsPromise={postsPromise} />
      </Suspense>
    </div>
  );
}

// 2. Conditional data fetching
function ConditionalData({ shouldFetch, id }: { 
  shouldFetch: boolean;
  id: number;
}) {
  const dataPromise = useMemo(
    () => (shouldFetch ? fetchData(id) : null),
    [shouldFetch, id]
  );
  
  if (!dataPromise) {
    return <div>Data not needed</div>;
  }
  
  return (
    <Suspense fallback={<Loading />}>
      <DataComponent dataPromise={dataPromise} />
    </Suspense>
  );
}

// 3. Cache promises for reuse
const promiseCache = new Map<string, Promise<any>>();

function getCachedPromise<T>(
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  if (!promiseCache.has(key)) {
    promiseCache.set(key, fetcher());
  }
  return promiseCache.get(key)!;
}

function CachedComponent({ id }: { id: string }) {
  const dataPromise = useMemo(
    () => getCachedPromise(`data-${id}`, () => fetchData(id)),
    [id]
  );
  
  const data = use(dataPromise);
  return <div>{data.content}</div>;
}

// 4. Retry pattern
function useRetryablePromise<T>(
  key: string,
  fetcher: () => Promise<T>,
  retries = 3
) {
  const [attempt, setAttempt] = useState(0);
  
  const promise = useMemo(() => {
    return fetcher().catch(error => {
      if (attempt < retries) {
        setAttempt(a => a + 1);
        throw error; // Will retry
      }
      throw error;
    });
  }, [key, attempt, retries]);
  
  return promise;
}

// 5. Parallel independent loading
function Dashboard() {
  const userPromise = useMemo(() => fetchUser(), []);
  const notificationsPromise = useMemo(() => fetchNotifications(), []);
  const statsPromise = useMemo(() => fetchStats(), []);
  
  return (
    <>
      <Suspense fallback={<UserSkeleton />}>
        <UserWidget userPromise={userPromise} />
      </Suspense>
      <Suspense fallback={<NotificationsSkeleton />}>
        <NotificationsWidget promise={notificationsPromise} />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsWidget promise={statsPromise} />
      </Suspense>
    </>
  );
}

Real-world patterns include caching promises, conditional fetching, retry logic, and parallel data loading. The use hook simplifies these patterns significantly.

Conclusion

The use hook revolutionizes async data handling in React. It simplifies promise-based data fetching, enables conditional context reading, and works seamlessly with Suspense. Use it for cleaner, more declarative async components. Remember to wrap in Suspense boundaries and handle errors with Error Boundaries.