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:
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
// 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 providedThe 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
// 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
// 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
// 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
// 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.