React Suspense and Concurrent Features REACT 18+
1. Suspense Component for Code Splitting (React.lazy)
| 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 and Async Operations
| 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 Fallbacks
| 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 (React 18+)
| 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 Hook for Non-blocking 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 (Server-Side Rendering)
| 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.