React Suspense and Concurrent Features REACT 18+
1. Suspense Component for Code Splitting
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| React.lazy() | lazy(() => import('./Comp')) |
Dynamic import for code splitting | Load components only when needed |
| Suspense | <Suspense fallback={...}> |
Show fallback while lazy component loads | Loading states for async components |
| fallback Prop | fallback={<Spinner/>} |
Component shown during loading | Skeleton screens, spinners, placeholders |
| Nested Suspense | <Suspense>...<Suspense> |
Multiple suspense boundaries at different levels | Granular loading states, progressive loading |
| Named Imports | lazy(() => import().then()) |
Load named exports with lazy | Non-default exports from modules |
Example: Basic code splitting with Suspense
import { lazy, Suspense } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<div>
<Header /> {/* Always loaded */}
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</div>
);
}
// Named export lazy loading
const MyComponent = lazy(() =>
import('./MyModule').then(module => ({
default: module.MyNamedExport
}))
);
Example: Nested Suspense boundaries for granular loading
const MainContent = lazy(() => import('./MainContent'));
const Sidebar = lazy(() => import('./Sidebar'));
const Comments = lazy(() => import('./Comments'));
function Page() {
return (
<div>
{/* Top-level suspense for entire page */}
<Suspense fallback={<PageSkeleton />}>
<div className="layout">
{/* Separate boundary for main content */}
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
{/* Nested boundary for comments */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments />
</Suspense>
</Suspense>
{/* Separate boundary for sidebar */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</div>
</Suspense>
</div>
);
}
// Route-based code splitting
function App() {
return (
<Router>
<Suspense fallback={<FullPageSpinner />}>
<Routes>
<Route path="/" element={lazy(() => import('./Home'))} />
<Route path="/about" element={lazy(() => import('./About'))} />
</Routes>
</Suspense>
</Router>
);
}
Code Splitting Best Practices: Split at route level first, use nested Suspense for progressive
loading, show meaningful fallbacks (skeletons better than spinners), preload critical routes, split large
third-party libraries separately.
2. Suspense for Data Fetching Patterns
| Pattern | Implementation | Description | Status |
|---|---|---|---|
| Suspense-enabled Libraries | React Query, SWR, Relay | Libraries with built-in Suspense support | Stable |
| use() Hook REACT 19 | const data = use(promise) |
Read promise value, suspends until resolved | React 19+ |
| Resource Pattern | Wrap promise in resource object | Manual Suspense implementation pattern | Advanced |
| Server Components | Async components on server | Native async/await in components | Next.js 13+ |
| Streaming SSR | renderToPipeableStream | Stream HTML with Suspense boundaries | React 18+ |
Example: Suspense with React Query
import { useQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
// Component that uses Suspense for data fetching
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
suspense: true // Enable Suspense mode
});
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Wrap with Suspense
function App() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={123} />
</Suspense>
);
}
// Multiple parallel data fetches with Suspense
function Dashboard() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<div className="dashboard">
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<RecentActivity />
</Suspense>
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
</div>
</Suspense>
);
}
Example: use() hook for Suspense data fetching (React 19)
import { use, Suspense } from 'react';
// Fetch function returns a promise
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Component using use() hook
function UserProfile({ userPromise }) {
// use() suspends component until promise resolves
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// Parent component
function App() {
// Start fetch immediately (not in component)
const userPromise = fetchUser(123);
return (
<Suspense fallback={<LoadingSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Conditional data fetching with use()
function ConditionalData({ showData }) {
if (!showData) {
return <div>Enable data display</div>;
}
// use() can be called conditionally (unlike hooks!)
const data = use(fetchData());
return <div>{data.content}</div>;
}
Warning: Suspense for data fetching requires libraries with Suspense integration (React Query,
SWR, Relay) or React 19's use() hook. Don't throw promises manually unless implementing a proper resource
pattern.
3. Error Boundaries with Suspense
| Concept | Implementation | Description | Use Case |
|---|---|---|---|
| Error Boundary Wrapper | Wrap Suspense with ErrorBoundary | Catch errors from suspended components | Handle loading failures gracefully |
| Separate Boundaries | ErrorBoundary per Suspense | Independent error handling for sections | Prevent whole page failure |
| Retry Logic | Reset error boundary state | Allow user to retry failed loads | Temporary network issues, transient errors |
| Error Fallback UI | Custom error components | User-friendly error messages | Better UX than blank screens |
Example: Error Boundary with Suspense integration
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
// Error fallback component with retry
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-container">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>
Try again
</button>
</div>
);
}
// Combined Error + Suspense boundary
function Page() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset any cached data
queryClient.resetQueries();
}}
>
<Suspense fallback={<LoadingSkeleton />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
// Multiple boundaries for granular error handling
function Dashboard() {
return (
<div className="dashboard">
{/* Each section has its own error boundary */}
<ErrorBoundary fallback={<SectionError section="Stats" />}>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError section="Activity" />}>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError section="Chart" />}>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
</ErrorBoundary>
</div>
);
}
Example: Reusable boundary component
// Combined ErrorBoundary + Suspense component
function AsyncBoundary({
children,
fallback,
errorFallback,
onReset
}) {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) =>
errorFallback ?
errorFallback(error, resetErrorBoundary) :
<DefaultErrorFallback error={error} reset={resetErrorBoundary} />
}
onReset={onReset}
>
<Suspense fallback={fallback || <DefaultLoadingFallback />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Usage - clean and simple
function App() {
return (
<AsyncBoundary
fallback={<PageSkeleton />}
errorFallback={(error, reset) => (
<PageError error={error} onRetry={reset} />
)}
>
<UserDashboard />
</AsyncBoundary>
);
}
Error Boundary + Suspense Pattern: Always wrap Suspense with ErrorBoundary, use granular
boundaries to prevent cascade failures, implement retry logic, show helpful error messages, log errors to
monitoring services.
4. Concurrent Rendering and Time Slicing
| Feature | Description | Benefit | Availability |
|---|---|---|---|
| Concurrent Mode | React can interrupt, pause, resume rendering | Keeps app responsive during expensive renders | React 18+ |
| Time Slicing | Break rendering work into chunks | Yield to browser between chunks for interactions | Automatic with React 18 |
| Automatic Batching | Batch multiple state updates together | Fewer re-renders, better performance | Enabled by default React 18 |
| Interruptible Rendering | React can pause low-priority work | High-priority updates don't get blocked | With concurrent features |
| Priority Levels | Urgent vs non-urgent updates | Keep UI responsive for user input | useTransition, useDeferredValue |
Example: Concurrent rendering with createRoot
import { createRoot } from 'react-dom/client';
// React 18+ concurrent mode (default with createRoot)
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// Automatic batching in React 18
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// React 18 batches these together automatically
// Even in async callbacks, timeouts, native events!
setCount(c => c + 1);
setFlag(f => !f);
// Only 1 re-render instead of 2
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? 'blue' : 'black' }}>
{count}
</h1>
</div>
);
}
// Opt-out of batching if needed (rare)
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// DOM updated immediately
flushSync(() => {
setFlag(f => !f);
});
// DOM updated again - 2 separate renders
}
Example: Visualizing concurrent rendering benefits
// Without concurrent features (React 17)
// Long render blocks everything
function HeavyList({ items }) {
// This blocks the entire UI during render
return (
<ul>
{items.map(item => (
<ExpensiveItem key={item.id} item={item} />
))}
</ul>
);
}
// User can't type or interact until render completes
// With concurrent features (React 18+)
function ResponsiveList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [displayItems, setDisplayItems] = useState(items);
function handleSearch(e) {
const value = e.target.value;
setQuery(value); // Urgent - updates immediately
// Non-urgent - can be interrupted
startTransition(() => {
const filtered = items.filter(item =>
item.name.includes(value)
);
setDisplayItems(filtered);
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending && <Spinner />}
<ul className={isPending ? 'loading' : ''}>
{displayItems.map(item => (
<ExpensiveItem key={item.id} item={item} />
))}
</ul>
</div>
);
}
// Input stays responsive - filtering happens in background
Concurrent Features: Enabled by default with createRoot in React 18, automatic batching
improves performance automatically, time slicing keeps UI responsive, use transitions for non-urgent updates, no
breaking changes for existing code.
5. useTransition for State Transitions
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useTransition Hook | const [isPending, start] = useTransition() |
Mark state updates as non-urgent transitions | Keep UI responsive during expensive updates |
| isPending Flag | isPending |
Boolean indicating if transition is in progress | Show loading indicators, disable UI temporarily |
| startTransition | startTransition(() => {...}) |
Function to wrap non-urgent state updates | Large list filters, tab switches, searches |
| Priority | Lower than user input | Can be interrupted by urgent updates | Heavy renders don't block interactions |
Example: useTransition for responsive search
import { useState, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e) {
const value = e.target.value;
// Urgent: Update input immediately
setQuery(value);
// Non-urgent: Update results (can be interrupted)
startTransition(() => {
// Expensive filtering
const filtered = allItems.filter(item =>
item.title.toLowerCase().includes(value.toLowerCase()) ||
item.description.toLowerCase().includes(value.toLowerCase())
);
setResults(filtered);
});
}
return (
<div>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="Search..."
/>
{isPending && <LoadingSpinner />}
<ul className={isPending ? 'opacity-50' : ''}>
{results.map(item => (
<SearchResultItem key={item.id} item={item} />
))}
</ul>
</div>
);
}
Example: Tab switching with transitions
function Tabs() {
const [activeTab, setActiveTab] = useState('home');
const [isPending, startTransition] = useTransition();
function handleTabClick(tab) {
// Mark tab content update as transition
startTransition(() => {
setActiveTab(tab);
});
}
return (
<div>
<div className="tab-buttons">
<button
onClick={() => handleTabClick('home')}
className={activeTab === 'home' ? 'active' : ''}
>
Home
</button>
<button
onClick={() => handleTabClick('posts')}
className={activeTab === 'posts' ? 'active' : ''}
>
Posts {isPending && '...'}
</button>
<button
onClick={() => handleTabClick('contact')}
className={activeTab === 'contact' ? 'active' : ''}
>
Contact
</button>
</div>
<div className={`tab-content ${isPending ? 'loading' : ''}`}>
{activeTab === 'home' && <HomeTab />}
{activeTab === 'posts' && <PostsTab />} {/* Expensive */}
{activeTab === 'contact' && <ContactTab />}
</div>
</div>
);
}
// Comparison: without useTransition
function SlowTabs() {
const [activeTab, setActiveTab] = useState('home');
// Tab clicks feel laggy because UI waits for expensive render
return (
<div>
<button onClick={() => setActiveTab('posts')}>
Posts
</button>
{/* UI freezes until PostsTab renders */}
{activeTab === 'posts' && <PostsTab />}
</div>
);
}
Example: Pagination with transitions
function PaginatedList() {
const [page, setPage] = useState(1);
const [isPending, startTransition] = useTransition();
// Fetch data for current page
const { data, loading } = useFetch(`/api/items?page=${page}`);
function goToPage(newPage) {
startTransition(() => {
setPage(newPage);
});
}
return (
<div>
{/* Show current data while transitioning */}
<div className={isPending ? 'transitioning' : ''}>
{loading ? (
<Spinner />
) : (
<ItemList items={data} />
)}
</div>
<div className="pagination">
<button
onClick={() => goToPage(page - 1)}
disabled={page === 1 || isPending}
>
Previous
</button>
<span>Page {page} {isPending && '(loading...)'}</span>
<button
onClick={() => goToPage(page + 1)}
disabled={isPending}
>
Next
</button>
</div>
</div>
);
}
useTransition Best Practices: Use for expensive updates that aren't time-sensitive, keep user
input responsive (don't wrap input onChange), show isPending state to user, works with Suspense boundaries, test
on low-end devices to see benefits.
6. Server Suspense and Streaming SSR
| Feature | API | Description | Use Case |
|---|---|---|---|
| Streaming SSR | renderToPipeableStream() |
Stream HTML to client progressively | Faster initial page load, better TTFB |
| Selective Hydration | Automatic with Suspense | Hydrate critical parts first | Interactive faster, better TTI |
| Server Suspense | <Suspense> on server | Show fallback while server fetches data | Stream page with loading states |
| Server Components | Async components (Next.js) | Await data directly in components | Zero-bundle data fetching |
| Progressive Loading | Nested Suspense boundaries | Stream page sections independently | Show content as it's ready |
Example: Node.js streaming SSR setup
import { renderToPipeableStream } from 'react-dom/server';
// Server-side streaming setup
app.get('/', (req, res) => {
const { pipe, abort } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/main.js'],
onShellReady() {
// Start streaming immediately
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
res.status(500).send('<h1>Server Error</h1>');
},
onAllReady() {
// All Suspense boundaries resolved
console.log('All content ready');
},
onError(error) {
console.error('Stream error:', error);
}
}
);
// Abort after timeout
setTimeout(() => abort(), 10000);
});
// App with Suspense boundaries
function App() {
return (
<html>
<head>
<title>Streaming SSR App</title>
</head>
<body>
<Header /> {/* Rendered immediately */}
{/* Stream this section separately */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Slow data fetch */}
</Suspense>
<Footer /> {/* Rendered immediately */}
</body>
</html>
);
}
Example: Next.js Server Components with Suspense
// app/page.tsx (Next.js 13+ App Router)
import { Suspense } from 'react';
// Server Component - can be async!
async function UserProfile({ userId }) {
// Fetch directly in component (server-side)
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json());
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
async function Posts() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Page with streaming sections
export default function Page() {
return (
<div>
<h1>My Page</h1>
{/* Profile streams first */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId="123" />
</Suspense>
{/* Posts stream independently */}
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
// HTML streams progressively as data arrives
Example: Selective hydration benefits
// Page with multiple sections
function App() {
return (
<div>
<Header /> {/* Hydrates first (small, fast) */}
{/* Heavy component wrapped in Suspense */}
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart /> {/* Hydrates later, doesn't block */}
</Suspense>
{/* User can interact with button before chart hydrates */}
<button onClick={handleClick}>
Click me (interactive immediately!)
</button>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Hydrates after chart */}
</Suspense>
</div>
);
}
// Benefits of selective hydration:
// 1. Faster Time to Interactive (TTI)
// 2. User can interact with parts before full hydration
// 3. Heavy components don't block critical interactions
// 4. Automatic priority based on user interaction
Streaming SSR Benefits: Faster TTFB (Time to First Byte), progressive page rendering, selective
hydration for better TTI, better perceived performance, works with Suspense boundaries, built into Next.js 13+
App Router.