
React Core Concurrent
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-core-concurrent is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-core-concurrent
- Frontend Development
- AI-coding skill
React Core Concurrent by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-core-concurrentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-core-concurrent
Quick Reference
Concurrent APIs at a Glance
| API | Purpose | Returns | React Version |
|---|---|---|---|
<Suspense> | Show fallback while children load | JSX boundary | 16.6+ (full in 18+) |
useTransition() | Mark updates as non-blocking + track pending | [isPending, startTransition] | 18+ |
startTransition() | Mark updates as non-blocking (no pending state) | void | 18+ |
useDeferredValue(value, initial?) | Defer re-renders for a value | deferred value | 18+ (initial in 19) |
lazy(load) | Code-split a component | lazy component | 16.6+ |
use(resource) | Read promise or context (conditional OK) | resolved value | 19+ |
API Signatures
// Suspense
<Suspense fallback={<Spinner />}>{children}</Suspense>
// useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => { setState(newValue); });
// Module-level startTransition
import { startTransition } from "react";
startTransition(() => { setState(newValue); });
// useDeferredValue
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19+
// lazy
const LazyComponent = lazy(() => import("./Component"));
// use (React 19+)
const value = use(promise);
const value = use(SomeContext);---
Critical Warnings
NEVER call lazy() inside a component body -- ALWAYS declare lazy components at module top level. Declaring inside a component resets state on every re-render.
NEVER use startTransition or useTransition for controlled text input updates -- transitions are interruptible and will cause input lag. Use useDeferredValue to defer downstream renders instead.
NEVER create new objects inside render and pass them to useDeferredValue -- ALWAYS pass primitives or objects created outside rendering. New object references on every render cause unnecessary background re-renders.
NEVER call use() inside a try-catch block -- it integrates with Suspense and Error Boundaries. Use promise.catch() or <ErrorBoundary> instead.
NEVER expect Suspense to detect data fetching in event handlers or useEffect -- ONLY Suspense-enabled data sources (frameworks, lazy(), use()) trigger Suspense boundaries.
NEVER wrap state updates after await without a new startTransition call -- updates scheduled after an await lose their transition context. ALWAYS wrap post-await state updates in a fresh startTransition.
---
Decision Tree: Which Concurrent Feature?
Need to show loading UI while content loads?
YES --> Use <Suspense> with a fallback
Need to load a component lazily?
YES --> Use React.lazy() + <Suspense>
Need to read a promise in a component? (React 19)
YES --> Use use(promise) inside <Suspense>
Need to fetch data with loading state?
YES --> Use Suspense-enabled framework (Next.js, Relay)
Need to keep UI responsive during a state update?
YES --> Do you control the state setter?
YES --> Do you need a pending indicator?
YES --> Use useTransition() (gives isPending)
NO --> Use startTransition() (simpler)
NO --> Use useDeferredValue(value)
Need to defer an expensive re-render?
YES --> Is the slow component receiving a prop?
YES --> Use useDeferredValue(prop) + memo() on the slow component
NO --> Use useTransition() around the state update
Need to prevent Suspense fallback from re-appearing?
YES --> Wrap navigation/update in startTransition()
Need code splitting / route-based splitting?
YES --> Use React.lazy() + <Suspense>---
Patterns
1. Suspense with Nested Boundaries
ALWAYS use nested <Suspense> boundaries to create progressive loading experiences. The outermost boundary catches the first suspension; inner boundaries handle independent sections.
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
</Suspense>Loading sequence: PageSkeleton shows until Header loads. Then Header appears, SidebarSkeleton and ContentSkeleton show independently until their children resolve.
2. Transitions for Navigation
ALWAYS use startTransition when navigating between views that suspend. This prevents hiding already-visible content behind a fallback.
import { useState, useTransition } from "react";
function Router(): JSX.Element {
const [page, setPage] = useState<string>("/home");
const [isPending, startTransition] = useTransition();
function navigate(url: string): void {
startTransition(() => {
setPage(url);
});
}
return (
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Nav onNavigate={navigate} />
<Suspense fallback={<PageLoader />}>
<PageContent page={page} />
</Suspense>
</div>
);
}3. Deferred Value for Search
ALWAYS pair useDeferredValue with memo() on the receiving component. Without memo(), the child re-renders immediately with the old value anyway, defeating the purpose.
import { useState, useDeferredValue, Suspense, memo } from "react";
function SearchPage(): JSX.Element {
const [query, setQuery] = useState<string>("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<p>Loading...</p>}>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</>
);
}
const SearchResults = memo(function SearchResults({ query }: { query: string }) {
// Expensive render or data fetch
const results = use(fetchResults(query));
return <ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
});4. Code Splitting with React.lazy
import { lazy, Suspense } from "react";
// ALWAYS at module top level
const Settings = lazy(() => import("./Settings"));
const Dashboard = lazy(() => import("./Dashboard"));
function App({ page }: { page: string }): JSX.Element {
return (
<Suspense fallback={<Loading />}>
{page === "settings" ? <Settings /> : <Dashboard />}
</Suspense>
);
}5. Suspense Boundary Reset with Key
ALWAYS use the key prop to reset a Suspense boundary when navigating to different content of the same type (e.g., different user profiles).
<Suspense fallback={<ProfileSkeleton />}>
<ProfilePage key={userId} userId={userId} />
</Suspense>---
Anti-Patterns
See references/anti-patterns.md for detailed examples.
Summary of NEVER rules:
- NEVER nest
startTransitioncalls expecting independent tracking -- use separateuseTransitionhooks - NEVER use
useDeferredValuewithoutmemo()on the consuming component - NEVER declare
lazy()inside component bodies - NEVER expect Suspense to catch event handler or useEffect fetches
- NEVER use transitions for urgent updates (text input, toggles)
---
Version Notes: React 18 vs React 19
| Feature | React 18 | React 19 |
|---|---|---|
<Suspense> | Full concurrent support | Improved: no longer re-mounts effects on reveal |
useTransition | Sync actions only | Supports async functions in startTransition |
startTransition | Sync actions only | Supports async functions |
useDeferredValue | useDeferredValue(value) | Adds optional initialValue parameter |
use() | Not available | New: reads promises and context conditionally |
| Streaming SSR | renderToPipeableStream | Same API, improved performance |
| Suspense for data | Framework-dependent | use(promise) enables direct promise reading |
React 19: use() Hook
// React 19 ONLY -- read a promise directly
import { use, Suspense } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }): JSX.Element {
const user = use(userPromise); // Suspends until resolved
return <h1>{user.name}</h1>;
}
// use() works in conditionals (unlike other hooks)
function MaybeThemed({ showTheme }: { showTheme: boolean }): JSX.Element {
if (showTheme) {
const theme = use(ThemeContext); // Allowed in conditionals
return <div className={theme}>Themed</div>;
}
return <div>Default</div>;
}React 19: useDeferredValue initialValue
// React 19 ONLY -- provide initial value for first render
const deferredQuery = useDeferredValue(query, ""); // "" on first render---
Reference Links
- references/examples.md -- Complete concurrent feature code examples
- references/patterns.md -- Suspense and transition patterns in depth
- references/anti-patterns.md -- Common concurrent feature mistakes
Official Sources
- https://react.dev/reference/react/Suspense
- https://react.dev/reference/react/useTransition
- https://react.dev/reference/react/useDeferredValue
- https://react.dev/reference/react/lazy
- https://react.dev/reference/react/use
- https://react.dev/reference/react/startTransition
- https://react.dev/reference/react-dom/server/renderToPipeableStream
react-core-concurrent: Anti-Patterns
Anti-Pattern 1: Declaring lazy() Inside Components
Problem: Declaring lazy() inside a component function causes the lazy component to be re-created on every render, resetting all state.
// BAD -- lazy() inside component body
function App(): JSX.Element {
const Settings = lazy(() => import("./Settings")); // Re-created every render!
return (
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
);
}
// GOOD -- lazy() at module top level
const Settings = lazy(() => import("./Settings")); // Created once
function App(): JSX.Element {
return (
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
);
}Rule: ALWAYS declare lazy() at module top level. React caches the resolved component per lazy reference -- a new reference means a new cache entry and a new load.
---
Anti-Pattern 2: useDeferredValue Without memo()
Problem: useDeferredValue defers the value, but the child component still re-renders with the old value on the first pass. Without memo(), the child re-renders twice (old value + new value) with no performance benefit.
// BAD -- ProductList re-renders on every keystroke despite deferral
function SearchPage(): JSX.Element {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ProductList filter={deferredQuery} /> {/* Re-renders anyway! */}
</>
);
}
function ProductList({ filter }: { filter: string }): JSX.Element {
// Expensive filtering runs on EVERY render
return <ul>{/* ... */}</ul>;
}
// GOOD -- memo() prevents re-render when deferredQuery hasn't changed
const ProductList = memo(function ProductList({ filter }: { filter: string }): JSX.Element {
return <ul>{/* ... */}</ul>;
});Rule: ALWAYS wrap the component receiving a deferred value in memo(). Without it, useDeferredValue provides zero performance benefit.
---
Anti-Pattern 3: Using Transitions for Text Input
Problem: Transitions are interruptible -- React deprioritizes them. For a controlled text input, this means the input value lags behind keystrokes, creating a broken typing experience.
// BAD -- input becomes unresponsive
function SearchForm(): JSX.Element {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
return (
<input
value={query}
onChange={(e) => {
startTransition(() => {
setQuery(e.target.value); // Input update is deprioritized!
});
}}
/>
);
}
// GOOD -- update input immediately, defer expensive downstream work
function SearchForm(): JSX.Element {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<Loading />}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}Rule: NEVER use startTransition for controlled input state updates. Use useDeferredValue to defer the downstream render instead.
---
Anti-Pattern 4: Expecting Suspense to Catch Effect/Event Handler Fetches
Problem: Suspense only catches suspensions from Suspense-enabled data sources (lazy(), use(), framework integrations). Data fetches initiated in useEffect or event handlers do NOT trigger Suspense.
// BAD -- useEffect fetch does NOT trigger Suspense
function UserProfile({ userId }: { userId: string }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser); // Suspense boundary will NEVER show fallback
}, [userId]);
if (!user) return <Loading />; // Manual loading state needed
return <h1>{user.name}</h1>;
}
// GOOD (React 19) -- use() integrates with Suspense
function UserProfile({ userPromise }: { userPromise: Promise<User> }): JSX.Element {
const user = use(userPromise); // Suspends properly
return <h1>{user.name}</h1>;
}Rule: NEVER expect Suspense to detect data fetching in useEffect or event handlers. Use use() (React 19), React.lazy(), or a Suspense-enabled framework.
---
Anti-Pattern 5: Missing Error Boundaries Around Data-Fetching Suspense
Problem: If a promise rejects inside a Suspense boundary without an Error Boundary, the error propagates up and can crash the entire application.
// BAD -- no Error Boundary, rejected promise crashes the app
function App(): JSX.Element {
return (
<Suspense fallback={<Loading />}>
<DataComponent /> {/* If this rejects, entire app crashes */}
</Suspense>
);
}
// GOOD -- Error Boundary catches rejected promises
function App(): JSX.Element {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}Rule: ALWAYS wrap data-fetching Suspense boundaries in an Error Boundary. This prevents a single failed request from taking down the entire component tree.
---
Anti-Pattern 6: Forgetting startTransition After await
Problem: In React 19 async transitions, state updates after an await lose their transition context. They become urgent updates and can cause Suspense fallbacks to re-appear.
// BAD -- post-await setState is NOT inside a transition
function SaveButton(): JSX.Element {
const [isPending, startTransition] = useTransition();
const [status, setStatus] = useState<string>("idle");
function handleSave(): void {
startTransition(async () => {
setStatus("saving"); // Inside transition
const result = await saveData();
setStatus("done"); // NOT inside transition! Becomes urgent update
});
}
return <button onClick={handleSave}>{status}</button>;
}
// GOOD -- wrap post-await updates in a new startTransition
function SaveButton(): JSX.Element {
const [isPending, startTransition] = useTransition();
const [status, setStatus] = useState<string>("idle");
function handleSave(): void {
startTransition(async () => {
setStatus("saving");
const result = await saveData();
startTransition(() => {
setStatus("done"); // Properly inside a transition
});
});
}
return <button onClick={handleSave}>{status}</button>;
}Rule: ALWAYS wrap state updates after await in a fresh startTransition call. The transition context is lost at every await boundary.
---
Anti-Pattern 7: Creating New Objects for useDeferredValue
Problem: Passing a new object reference on every render to useDeferredValue triggers unnecessary background re-renders because React sees a "new" value every time.
// BAD -- new object on every render
function FilteredList({ items }: { items: Item[] }): JSX.Element {
const [query, setQuery] = useState("");
// New object created every render -- useDeferredValue always schedules background work
const deferredFilter = useDeferredValue({ query, items });
return <ExpensiveList filter={deferredFilter} />;
}
// GOOD -- pass primitive value
function FilteredList({ items }: { items: Item[] }): JSX.Element {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query); // Primitive, stable reference
return <ExpensiveList query={deferredQuery} items={items} />;
}Rule: ALWAYS pass primitive values or objects created outside rendering to useDeferredValue. New object references on every render defeat the deferred comparison.
---
Anti-Pattern 8: Over-Granular Suspense Boundaries
Problem: Wrapping every single component in its own Suspense boundary creates a "popcorn" loading effect where dozens of skeletons appear and resolve independently, creating a jarring user experience.
// BAD -- every component has its own boundary
function Dashboard(): JSX.Element {
return (
<div>
<Suspense fallback={<Skeleton />}>
<UserName />
</Suspense>
<Suspense fallback={<Skeleton />}>
<UserAvatar />
</Suspense>
<Suspense fallback={<Skeleton />}>
<UserEmail />
</Suspense>
<Suspense fallback={<Skeleton />}>
<UserBio />
</Suspense>
</div>
);
}
// GOOD -- group related content in a single boundary
function Dashboard(): JSX.Element {
return (
<div>
<Suspense fallback={<UserCardSkeleton />}>
<UserName />
<UserAvatar />
<UserEmail />
<UserBio />
</Suspense>
</div>
);
}Rule: NEVER make Suspense boundaries finer than the intended loading experience. Group components that should appear together inside the SAME boundary.
---
Anti-Pattern 9: Using use() in Try-Catch Blocks
Problem: use() integrates with Suspense and Error Boundaries by throwing. Wrapping it in try-catch intercepts these throws and breaks the Suspense/Error Boundary mechanism.
// BAD -- try-catch intercepts Suspense throws
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }): JSX.Element {
try {
const data = use(dataPromise); // Throws for Suspense -- caught by try-catch!
return <div>{data.value}</div>;
} catch (e) {
return <div>Error</div>; // Catches Suspense throws too!
}
}
// GOOD -- let Suspense and Error Boundaries handle it
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }): JSX.Element {
const data = use(dataPromise); // Throws handled by Suspense + ErrorBoundary
return <div>{data.value}</div>;
}
// Usage
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataDisplay dataPromise={fetchData()} />
</Suspense>
</ErrorBoundary>Rule: NEVER call use() inside a try-catch block. Use Error Boundaries for error handling and Suspense for loading states.
---
Anti-Pattern 10: Nesting startTransition Expecting Independent Tracking
Problem: Nesting startTransition calls from the same useTransition hook does not create independent transitions. The inner call shares the outer isPending state.
// BAD -- nested transitions share isPending
function MultiAction(): JSX.Element {
const [isPending, startTransition] = useTransition();
function handleClick(): void {
startTransition(() => {
setA(newA);
startTransition(() => {
setB(newB); // Same isPending as outer transition
});
});
}
return <div>{isPending ? "Loading..." : "Ready"}</div>;
}
// GOOD -- separate useTransition hooks for independent tracking
function MultiAction(): JSX.Element {
const [isPendingA, startTransitionA] = useTransition();
const [isPendingB, startTransitionB] = useTransition();
function handleClick(): void {
startTransitionA(() => setA(newA));
startTransitionB(() => setB(newB));
}
return (
<div>
{isPendingA && <span>Loading A...</span>}
{isPendingB && <span>Loading B...</span>}
</div>
);
}Rule: NEVER nest startTransition calls expecting independent pending state tracking. Use separate useTransition hooks for independently tracked transitions.
react-core-concurrent: Code Examples
1. Basic Suspense with Data Fetching
import { Suspense } from "react";
interface Album {
id: string;
title: string;
year: number;
}
// Assumes a Suspense-enabled data source (framework or use() in React 19)
function AlbumList({ artistId }: { artistId: string }): JSX.Element {
const albums = useSuspenseData<Album[]>(`/api/artists/${artistId}/albums`);
return (
<ul>
{albums.map((album) => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
function ArtistPage({ artistId }: { artistId: string }): JSX.Element {
return (
<div>
<h1>Albums</h1>
<Suspense fallback={<p>Loading albums...</p>}>
<AlbumList artistId={artistId} />
</Suspense>
</div>
);
}---
2. Progressive Loading with Nested Suspense
import { Suspense } from "react";
function DashboardPage(): JSX.Element {
return (
<Suspense fallback={<FullPageSpinner />}>
<DashboardHeader />
<div className="dashboard-grid">
<Suspense fallback={<CardSkeleton title="Revenue" />}>
<RevenueCard />
</Suspense>
<Suspense fallback={<CardSkeleton title="Users" />}>
<UsersCard />
</Suspense>
<Suspense fallback={<CardSkeleton title="Activity" />}>
<ActivityFeed />
</Suspense>
</div>
</Suspense>
);
}
function FullPageSpinner(): JSX.Element {
return <div className="spinner" aria-label="Loading dashboard" />;
}
function CardSkeleton({ title }: { title: string }): JSX.Element {
return (
<div className="card skeleton">
<h3>{title}</h3>
<div className="skeleton-bar" />
</div>
);
}Loading sequence: 1. FullPageSpinner shows until DashboardHeader resolves 2. Header appears; each card shows its own skeleton independently 3. Cards appear as their data arrives (in any order)
---
3. useTransition for Tab Navigation
import { useState, useTransition, Suspense } from "react";
type TabId = "posts" | "comments" | "photos";
function TabContainer(): JSX.Element {
const [tab, setTab] = useState<TabId>("posts");
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: TabId): void {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav>
{(["posts", "comments", "photos"] as const).map((t) => (
<button
key={t}
onClick={() => selectTab(t)}
className={tab === t ? "active" : ""}
disabled={isPending && tab !== t}
>
{t}
</button>
))}
</nav>
<div style={{ opacity: isPending ? 0.6 : 1, transition: "opacity 0.2s" }}>
<Suspense fallback={<TabSkeleton />}>
<TabContent tab={tab} />
</Suspense>
</div>
</div>
);
}
function TabContent({ tab }: { tab: TabId }): JSX.Element {
switch (tab) {
case "posts":
return <PostsTab />;
case "comments":
return <CommentsTab />;
case "photos":
return <PhotosTab />;
}
}Why this works: startTransition tells React the tab update is non-urgent. React keeps showing the current tab (dimmed via isPending) instead of immediately showing the Suspense fallback.
---
4. useTransition with Async Actions (React 19)
import { useState, useTransition } from "react";
interface FormData {
name: string;
email: string;
}
function ProfileForm(): JSX.Element {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [profile, setProfile] = useState<FormData>({ name: "", email: "" });
async function handleSubmit(formData: FormData): Promise<void> {
setError(null);
startTransition(async () => {
const result = await saveProfile(formData);
// IMPORTANT: wrap post-await state updates in startTransition
startTransition(() => {
if (result.error) {
setError(result.error);
} else {
setProfile(result.data);
}
});
});
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(profile); }}>
<input
value={profile.name}
onChange={(e) => setProfile((p) => ({ ...p, name: e.target.value }))}
/>
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}---
5. useDeferredValue for Expensive List Filtering
import { useState, useDeferredValue, memo } from "react";
interface Product {
id: string;
name: string;
category: string;
}
function ProductSearch({ products }: { products: Product[] }): JSX.Element {
const [filter, setFilter] = useState<string>("");
const deferredFilter = useDeferredValue(filter);
const isStale = filter !== deferredFilter;
return (
<div>
<input
type="search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter products..."
/>
<div style={{ opacity: isStale ? 0.5 : 1, transition: "opacity 0.15s" }}>
<ProductList products={products} filter={deferredFilter} />
</div>
</div>
);
}
// CRITICAL: memo() is required for useDeferredValue to provide any benefit
const ProductList = memo(function ProductList({
products,
filter,
}: {
products: Product[];
filter: string;
}): JSX.Element {
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<ul>
{filtered.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
});How it works: 1. User types in the input -- filter updates immediately, input stays responsive 2. deferredFilter lags behind, so ProductList re-renders with the old value first 3. React schedules a background re-render with the new deferredFilter 4. If the user types again before the background render completes, React abandons it and starts fresh
---
6. useDeferredValue with initialValue (React 19)
import { useDeferredValue, Suspense } from "react";
function SearchPage(): JSX.Element {
const [query, setQuery] = useState<string>("");
// On first render, deferredQuery is "" (the initial value)
// On subsequent renders, it defers to the previous query value
const deferredQuery = useDeferredValue(query, "");
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<p>Loading results...</p>}>
{deferredQuery && <SearchResults query={deferredQuery} />}
</Suspense>
</>
);
}---
7. Code Splitting with React.lazy
import { lazy, Suspense, useState, useTransition } from "react";
// ALWAYS declare at module top level
const AdminPanel = lazy(() => import("./AdminPanel"));
const UserDashboard = lazy(() => import("./UserDashboard"));
const Analytics = lazy(() => import("./Analytics"));
type Route = "dashboard" | "admin" | "analytics";
function App(): JSX.Element {
const [route, setRoute] = useState<Route>("dashboard");
const [isPending, startTransition] = useTransition();
function navigate(to: Route): void {
startTransition(() => {
setRoute(to);
});
}
return (
<div>
<nav>
<button onClick={() => navigate("dashboard")}>Dashboard</button>
<button onClick={() => navigate("admin")}>Admin</button>
<button onClick={() => navigate("analytics")}>Analytics</button>
</nav>
{isPending && <div className="loading-bar" />}
<Suspense fallback={<PageSkeleton />}>
{route === "dashboard" && <UserDashboard />}
{route === "admin" && <AdminPanel />}
{route === "analytics" && <Analytics />}
</Suspense>
</div>
);
}---
8. use() Hook for Promise Reading (React 19)
import { use, Suspense } from "react";
interface User {
id: string;
name: string;
email: string;
}
// Parent creates the promise and passes it down
function UserPage({ userId }: { userId: string }): JSX.Element {
// IMPORTANT: create promise outside the suspending component
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Child reads the promise with use()
function UserProfile({ userPromise }: { userPromise: Promise<User> }): JSX.Element {
const user = use(userPromise); // Suspends until resolved
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}---
9. use() for Conditional Context (React 19)
import { use, createContext } from "react";
interface Theme {
primary: string;
background: string;
}
const ThemeContext = createContext<Theme>({
primary: "#007bff",
background: "#ffffff",
});
// use() can be called conditionally -- unlike useContext()
function ThemedButton({
themed,
children,
}: {
themed: boolean;
children: React.ReactNode;
}): JSX.Element {
if (themed) {
const theme = use(ThemeContext);
return (
<button style={{ backgroundColor: theme.primary, color: "#fff" }}>
{children}
</button>
);
}
return <button>{children}</button>;
}---
10. Streaming SSR with Suspense
// server.ts (Node.js)
import { renderToPipeableStream } from "react-dom/server";
import App from "./App";
function handleRequest(req: Request, res: Response): void {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ["/client.js"],
onShellReady() {
// Shell (content outside Suspense boundaries) is ready
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
pipe(res);
},
onShellError(error: unknown) {
// Critical error -- shell could not render
res.statusCode = 500;
res.send("Server error");
},
onError(error: unknown) {
// Non-critical -- logged but stream continues
console.error(error);
},
});
}How streaming SSR works with Suspense: 1. React renders the shell (everything outside <Suspense> boundaries) immediately 2. Suspense fallbacks are included as placeholder HTML 3. When suspended content resolves, React streams additional HTML chunks 4. Client-side hydration replaces fallbacks with real content 5. Selective hydration prioritizes interactive regions the user engages with
---
11. Error Handling with Suspense and Error Boundaries
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DataSection(): JSX.Element {
return (
<ErrorBoundary
fallback={<p>Something went wrong loading data.</p>}
onError={(error) => logError(error)}
>
<Suspense fallback={<DataSkeleton />}>
<DataDisplay />
</Suspense>
</ErrorBoundary>
);
}ALWAYS wrap Suspense boundaries in Error Boundaries when fetching data. A rejected promise without an Error Boundary crashes the entire component tree.
---
12. Suspense Key Reset for Profile Navigation
import { Suspense } from "react";
function ProfilePage({ userId }: { userId: string }): JSX.Element {
return (
// key forces Suspense boundary to reset when userId changes
<Suspense key={userId} fallback={<ProfileSkeleton />}>
<ProfileContent userId={userId} />
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
</Suspense>
);
}Without key, React would try to show stale content from the previous user while loading. With key, it shows the fallback immediately for a clean transition.
react-core-concurrent: Suspense and Transition Patterns
Pattern 1: Progressive Disclosure Loading
When to use: Complex pages with multiple independent data sections.
Strategy: Use nested Suspense boundaries so each section loads independently. The outermost boundary covers the page shell; inner boundaries cover individual widgets.
<Suspense fallback={<AppShell />}>
<Navigation />
<main>
<Suspense fallback={<HeroSkeleton />}>
<HeroSection />
</Suspense>
<div className="grid">
<Suspense fallback={<WidgetSkeleton />}>
<StatsWidget />
</Suspense>
<Suspense fallback={<WidgetSkeleton />}>
<ChartWidget />
</Suspense>
<Suspense fallback={<WidgetSkeleton />}>
<RecentActivity />
</Suspense>
</div>
</main>
</Suspense>Design rules:
- ALWAYS coordinate Suspense boundary placement with the design team
- NEVER make boundaries finer than the intended loading experience
- Group components that should appear together inside the SAME boundary
- Use independent boundaries for sections that can load at different speeds
---
Pattern 2: Transition-Guarded Navigation
When to use: Route transitions where you want to keep the current page visible instead of showing a loading spinner.
Strategy: Wrap route state updates in startTransition. The current view remains visible (optionally dimmed) while the next page loads.
import { useState, useTransition, Suspense } from "react";
function Router(): JSX.Element {
const [currentRoute, setRoute] = useState<string>("/home");
const [isPending, startTransition] = useTransition();
function navigate(path: string): void {
startTransition(() => {
setRoute(path);
});
}
return (
<>
<header style={{ opacity: isPending ? 0.7 : 1 }}>
<NavBar onNavigate={navigate} currentRoute={currentRoute} />
{isPending && <ProgressBar />}
</header>
<Suspense fallback={<PageLoader />}>
<RouteContent route={currentRoute} />
</Suspense>
</>
);
}Key behavior: When startTransition wraps the state update: 1. React does NOT immediately show the Suspense fallback 2. The current content stays visible (stale but complete) 3. isPending becomes true so you can show a progress indicator 4. Once the new content is ready, React swaps it in atomically
ALWAYS use this pattern when navigating between pages that use Suspense for data fetching. Without it, users see a jarring flash of the fallback skeleton.
---
Pattern 3: Deferred Search with Stale Indication
When to use: Search inputs where typing must remain responsive but results are expensive to compute or fetch.
Strategy: Use useDeferredValue on the search query. The input updates immediately; results re-render in the background with the deferred value.
import { useState, useDeferredValue, Suspense, memo } from "react";
function SearchPage(): JSX.Element {
const [query, setQuery] = useState<string>("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<Suspense fallback={<ResultsSkeleton />}>
<div
style={{
opacity: isStale ? 0.5 : 1,
transition: "opacity 0.2s",
}}
>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</div>
);
}
// CRITICAL: must be memoized for useDeferredValue to skip re-renders
const SearchResults = memo(function SearchResults({
query,
}: {
query: string;
}): JSX.Element {
// ...expensive render or data fetch
});Three-phase render cycle: 1. Immediate: Input updates, query changes, deferredQuery keeps old value 2. Background: React re-renders with new deferredQuery (interruptible) 3. Commit: New results appear, isStale becomes false, opacity returns to 1
ALWAYS compare query !== deferredQuery to detect staleness and dim the UI accordingly.
---
Pattern 4: Optimistic UI with Transitions
When to use: User actions that trigger server requests where you want immediate visual feedback.
Strategy: Update UI optimistically, wrap the server call in startTransition, and revert on error.
import { useState, useTransition, useOptimistic } from "react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoList({ todos }: { todos: Todo[] }): JSX.Element {
const [isPending, startTransition] = useTransition();
const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos);
async function toggleTodo(id: string): Promise<void> {
startTransition(async () => {
// Show optimistic update immediately
setOptimisticTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
// Server request happens in the background
await updateTodoOnServer(id);
});
}
return (
<ul>
{optimisticTodos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ opacity: isPending ? 0.8 : 1 }}
>
{todo.completed ? "Done" : "Todo"}: {todo.text}
</li>
))}
</ul>
);
}---
Pattern 5: Code Splitting by Route
When to use: Single-page applications with multiple routes where you want to load route components on demand.
Strategy: Use React.lazy for each route component. Combine with startTransition for smooth transitions.
import { lazy, Suspense, useState, useTransition } from "react";
// ALWAYS at module top level
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Settings = lazy(() => import("./pages/Settings"));
const routes: Record<string, React.LazyExoticComponent<React.ComponentType>> = {
"/": Home,
"/about": About,
"/settings": Settings,
};
function App(): JSX.Element {
const [path, setPath] = useState<string>("/");
const [isPending, startTransition] = useTransition();
const PageComponent = routes[path] ?? Home;
return (
<div>
<nav>
{Object.keys(routes).map((route) => (
<a
key={route}
href={route}
onClick={(e) => {
e.preventDefault();
startTransition(() => setPath(route));
}}
>
{route}
</a>
))}
</nav>
{isPending && <TopLoadingBar />}
<Suspense fallback={<PageSkeleton />}>
<PageComponent />
</Suspense>
</div>
);
}---
Pattern 6: Server-to-Client Promise Streaming (React 19)
When to use: Server Components passing data promises to Client Components.
Strategy: Create the promise in the Server Component, pass it as a prop, and read it with use() inside a Suspense boundary on the client.
// Server Component (e.g., Next.js app router)
export default async function Page({ params }: { params: { id: string } }) {
// Create promise WITHOUT awaiting -- let client stream it
const dataPromise = fetchProjectData(params.id);
return (
<Suspense fallback={<ProjectSkeleton />}>
<ProjectDetails dataPromise={dataPromise} />
</Suspense>
);
}
// Client Component
"use client";
import { use } from "react";
function ProjectDetails({
dataPromise,
}: {
dataPromise: Promise<ProjectData>;
}): JSX.Element {
const data = use(dataPromise); // Suspends until resolved
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
);
}Key advantage: The server starts the data fetch immediately. The client receives a streaming promise -- no client-side waterfall.
---
Pattern 7: Suspense with Error Recovery
When to use: Any data-fetching Suspense boundary where failures are possible.
Strategy: ALWAYS wrap data-fetching Suspense boundaries in an Error Boundary. Provide a retry mechanism.
import { Suspense, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DataSection(): JSX.Element {
const [retryKey, setRetryKey] = useState<number>(0);
return (
<ErrorBoundary
key={retryKey}
fallback={
<div>
<p>Failed to load data.</p>
<button onClick={() => setRetryKey((k) => k + 1)}>Retry</button>
</div>
}
>
<Suspense fallback={<DataSkeleton />}>
<DataDisplay />
</Suspense>
</ErrorBoundary>
);
}Changing the key on ErrorBoundary resets its state, causing the children to re-mount and re-fetch.
---
Pattern 8: Selective Hydration Priority
When to use: Server-rendered pages with multiple interactive regions.
Strategy: Wrap each interactive region in its own Suspense boundary. React hydrates regions based on user interaction priority.
// Server-rendered layout
function Page(): JSX.Element {
return (
<div>
<StaticHeader />
<Suspense fallback={<NavSkeleton />}>
<InteractiveNav />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<InteractiveContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<InteractiveSidebar />
</Suspense>
</div>
);
}Hydration behavior: 1. React streams HTML for all sections (user sees complete page fast) 2. React starts hydrating sections as JS bundles arrive 3. If user clicks on InteractiveSidebar before it hydrates, React prioritizes that region 4. Discrete events (clicks) are replayed after hydration completes
ALWAYS use separate Suspense boundaries for independent interactive regions in SSR to enable selective hydration.