Overview
React’s concurrent features enable better user experiences by allowing React to interrupt rendering work to handle higher-priority updates. These features are particularly valuable in Next.js applications for creating responsive interfaces.
What are Concurrent Features?
Concurrent features allow React to work on multiple tasks simultaneously and prioritize urgent updates over less critical ones.
Concurrent Benefits Better responsiveness Improved user experience Non-blocking updates Priority-based rendering
Concurrent Features Automatic batching Transitions Suspense improvements Concurrent rendering
Automatic Batching
React 18 automatically batches state updates, reducing the number of re-renders and improving performance.
Before React 18
// Multiple re-renders
function handleClick () {
setCount (( c ) => c + 1 ); // Re-render 1
setFlag (( f ) => ! f ); // Re-render 2
setData (( d ) => d + 1 ); // Re-render 3
}
After React 18
// Single re-render (automatic batching)
function handleClick () {
setCount (( c ) => c + 1 ); // Batched
setFlag (( f ) => ! f ); // Batched
setData (( d ) => d + 1 ); // Batched
// Only one re-render
}
Batching in Event Handlers
"use client" ;
import { useState } from "react" ;
export default function Counter () {
const [ count , setCount ] = useState ( 0 );
const [ flag , setFlag ] = useState ( false );
const handleClick = () => {
// These updates are automatically batched
setCount (( c ) => c + 1 );
setFlag (( f ) => ! f );
// Only one re-render will occur
console . log ( "Component will re-render once" );
};
return (
< div >
< p > Count : { count }</ p >
< p > Flag : { flag ? "true" : "false" }</ p >
< button onClick = { handleClick } > Update Both </ button >
</ div >
);
}
Transitions
Transitions allow you to mark state updates as non-urgent, enabling React to interrupt them for more important updates.
Basic Transitions
"use client" ;
import { useState , useTransition } from "react" ;
export default function SearchResults () {
const [ query , setQuery ] = useState ( "" );
const [ results , setResults ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
const handleSearch = ( newQuery : string ) => {
setQuery ( newQuery ); // Urgent update
startTransition (() => {
// Non-urgent update
setResults ( expensiveSearch ( newQuery ));
});
};
return (
< div >
< input
value = { query }
onChange = {(e) => handleSearch (e.target.value)}
placeholder = "Search..."
/>
{ isPending && < div > Searching ...</ div >}
< div className = "results" >
{ results . map (( result ) => (
< div key = {result. id } > {result. title } </ div >
))}
</ div >
</ div >
);
}
Transitions with useDeferredValue
"use client" ;
import { useState , useDeferredValue , useMemo } from "react" ;
export default function ProductList () {
const [ query , setQuery ] = useState ( "" );
const deferredQuery = useDeferredValue ( query );
const products = useMemo (() => {
return expensiveProductSearch ( deferredQuery );
}, [ deferredQuery ]);
return (
< div >
< input
value = { query }
onChange = {(e) => setQuery (e.target.value)}
placeholder = "Search products..."
/>
< div className = "products" >
{ products . map (( product ) => (
< div key = {product. id } className = "product" >
< h3 >{product. name } </ h3 >
< p >{product. description } </ p >
</ div >
))}
</ div >
</ div >
);
}
Concurrent Rendering
Interruptible Rendering
"use client" ;
import { useState , useTransition } from "react" ;
export default function DataTable () {
const [ data , setData ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
const [ filter , setFilter ] = useState ( "" );
const handleFilterChange = ( newFilter : string ) => {
setFilter ( newFilter ); // Urgent update
startTransition (() => {
// Non-urgent update - can be interrupted
const filteredData = expensiveFilter ( data , newFilter );
setData ( filteredData );
});
};
return (
< div >
< input
value = { filter }
onChange = {(e) => handleFilterChange (e.target.value)}
placeholder = "Filter data..."
/>
{ isPending && < div > Filtering ...</ div >}
< table >
< tbody >
{ data . map (( row ) => (
< tr key = {row. id } >
< td >{row. name } </ td >
< td >{row. value } </ td >
</ tr >
))}
</ tbody >
</ table >
</ div >
);
}
Advanced Concurrent Patterns
Priority Updates
"use client" ;
import { useState , useTransition , useDeferredValue } from "react" ;
export default function Dashboard () {
const [ urgentData , setUrgentData ] = useState ( null );
const [ heavyData , setHeavyData ] = useState ( null );
const [ isPending , startTransition ] = useTransition ();
const deferredHeavyData = useDeferredValue ( heavyData );
const handleUrgentUpdate = () => {
// This update has high priority
setUrgentData ( fetchUrgentData ());
};
const handleHeavyUpdate = () => {
// This update has low priority
startTransition (() => {
setHeavyData ( expensiveCalculation ());
});
};
return (
< div className = "dashboard" >
< div className = "urgent-section" >
< h2 > Urgent Updates </ h2 >
< button onClick = { handleUrgentUpdate } > Update Urgent Data </ button >
{ urgentData && < div >{ urgentData }</ div >}
</ div >
< div className = "heavy-section" >
< h2 > Heavy Updates </ h2 >
< button onClick = { handleHeavyUpdate } > Update Heavy Data </ button >
{ isPending && < div > Processing ...</ div >}
{ deferredHeavyData && < div >{ deferredHeavyData }</ div >}
</ div >
</ div >
);
}
Concurrent Data Fetching
"use client" ;
import { useState , useTransition , useDeferredValue } from "react" ;
export default function SearchInterface () {
const [ query , setQuery ] = useState ( "" );
const [ results , setResults ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
const deferredQuery = useDeferredValue ( query );
const handleSearch = async ( searchQuery : string ) => {
startTransition ( async () => {
const searchResults = await fetchSearchResults ( searchQuery );
setResults ( searchResults );
});
};
return (
< div >
< input
value = { query }
onChange = {(e) => {
setQuery ( e . target . value );
handleSearch ( e . target . value );
}}
placeholder = "Search..."
/>
{ isPending && < div > Searching ...</ div >}
< div className = "results" >
{ results . map (( result ) => (
< div key = {result. id } className = "result" >
< h3 >{result. title } </ h3 >
< p >{result. description } </ p >
</ div >
))}
</ div >
</ div >
);
}
Optimizing Heavy Components
"use client" ;
import { useState , useTransition , useDeferredValue , useMemo } from "react" ;
export default function LargeDataTable () {
const [ data , setData ] = useState ([]);
const [ filter , setFilter ] = useState ( "" );
const [ sortBy , setSortBy ] = useState ( "name" );
const [ isPending , startTransition ] = useTransition ();
const deferredFilter = useDeferredValue ( filter );
const deferredSortBy = useDeferredValue ( sortBy );
const processedData = useMemo (() => {
return expensiveDataProcessing ( data , deferredFilter , deferredSortBy );
}, [ data , deferredFilter , deferredSortBy ]);
const handleFilterChange = ( newFilter : string ) => {
setFilter ( newFilter );
};
const handleSortChange = ( newSortBy : string ) => {
setSortBy ( newSortBy );
};
return (
< div >
< div className = "controls" >
< input
value = { filter }
onChange = {(e) => handleFilterChange (e.target.value)}
placeholder = "Filter..."
/>
< select
value = { sortBy }
onChange = {(e) => handleSortChange (e.target.value)} >
< option value = "name" > Name </ option >
< option value = "date" > Date </ option >
< option value = "value" > Value </ option >
</ select >
</ div >
{ isPending && < div > Processing data ...</ div >}
< table >
< thead >
< tr >
< th > Name </ th >
< th > Date </ th >
< th > Value </ th >
</ tr >
</ thead >
< tbody >
{ processedData . map (( row ) => (
< tr key = {row. id } >
< td >{row. name } </ td >
< td >{row. date } </ td >
< td >{row. value } </ td >
</ tr >
))}
</ tbody >
</ table >
</ div >
);
}
Real-World Examples
Search with Debouncing
"use client" ;
import { useState , useTransition , useDeferredValue , useEffect } from "react" ;
export default function SearchPage () {
const [ query , setQuery ] = useState ( "" );
const [ results , setResults ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
const deferredQuery = useDeferredValue ( query );
useEffect (() => {
if ( deferredQuery ) {
startTransition ( async () => {
const searchResults = await searchAPI ( deferredQuery );
setResults ( searchResults );
});
}
}, [ deferredQuery ]);
return (
< div >
< input
value = { query }
onChange = {(e) => setQuery (e.target.value)}
placeholder = "Search..."
/>
{ isPending && < div > Searching ...</ div >}
< div className = "results" >
{ results . map (( result ) => (
< div key = {result. id } className = "result" >
< h3 >{result. title } </ h3 >
< p >{result. description } </ p >
</ div >
))}
</ div >
</ div >
);
}
"use client" ;
import { useState , useTransition , useDeferredValue } from "react" ;
export default function ContactForm () {
const [ formData , setFormData ] = useState ({
name: "" ,
email: "" ,
message: "" ,
});
const [ validation , setValidation ] = useState ({});
const [ isPending , startTransition ] = useTransition ();
const deferredFormData = useDeferredValue ( formData );
const handleInputChange = ( field : string , value : string ) => {
setFormData (( prev ) => ({ ... prev , [field]: value }));
// Heavy validation in transition
startTransition (() => {
const validationResult = expensiveValidation ( deferredFormData );
setValidation ( validationResult );
});
};
return (
< form >
< div >
< input
type = "text"
value = {formData. name }
onChange = {(e) => handleInputChange ( "name" , e.target.value)}
placeholder = "Name"
/>
{ validation . name && < span className = "error" > {validation. name } </ span > }
</ div >
< div >
< input
type = "email"
value = {formData. email }
onChange = {(e) => handleInputChange ( "email" , e.target.value)}
placeholder = "Email"
/>
{ validation . email && < span className = "error" > {validation. email } </ span > }
</ div >
< div >
< textarea
value = {formData. message }
onChange = {(e) => handleInputChange ( "message" , e.target.value)}
placeholder = "Message"
/>
{ validation . message && (
< span className = "error" > {validation. message } </ span >
)}
</ div >
{ isPending && < div > Validating ...</ div >}
< button type = "submit" > Submit </ button >
</ form >
);
}
Best Practices
Concurrent Feature Usage
Identify Heavy Operations
Find operations that can block the UI
Use Transitions
Wrap heavy operations in startTransition
Defer Values
Use useDeferredValue for expensive computations
Provide Feedback
Show loading states during transitions
Batching Benefits Reduces re-renders Improves performance Automatic in React 18 Works in event handlers
Transition Benefits Non-blocking updates Better responsiveness Priority-based rendering Improved user experience
Common Patterns
Progressive Loading
"use client" ;
import { useState , useTransition } from "react" ;
export default function ProgressiveLoader () {
const [ data , setData ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
const loadData = () => {
startTransition (() => {
// Load data progressively
const newData = generateLargeDataset ();
setData ( newData );
});
};
return (
< div >
< button onClick = { loadData } > Load Data </ button >
{ isPending && < div > Loading data ...</ div >}
< div className = "data-grid" >
{ data . map (( item ) => (
< div key = {item. id } className = "data-item" >
{ item . content }
</ div >
))}
</ div >
</ div >
);
}
Responsive Updates
"use client" ;
import { useState , useTransition , useDeferredValue } from "react" ;
export default function ResponsiveInterface () {
const [ input , setInput ] = useState ( "" );
const [ output , setOutput ] = useState ( "" );
const [ isPending , startTransition ] = useTransition ();
const deferredInput = useDeferredValue ( input );
const processInput = ( value : string ) => {
startTransition (() => {
const processed = expensiveProcessing ( value );
setOutput ( processed );
});
};
return (
< div >
< input
value = { input }
onChange = {(e) => {
setInput ( e . target . value );
processInput ( e . target . value );
}}
placeholder = "Type something..."
/>
{ isPending && < div > Processing ...</ div >}
< div className = "output" > { output } </ div >
</ div >
);
}
Troubleshooting
Problem : startTransition not improving responsiveness
Solution : Ensure the operation inside startTransition is actually expensive and can be interrupted
Problem : Multiple re-renders still occurring Solution : Check that
updates are happening in event handlers or React-managed contexts
Problem : Concurrent features not providing expected performance benefits
Solution : Profile the application to identify actual bottlenecks
Problem : State updates being lost during transitions
Solution : Ensure state updates are properly batched and not conflicting
Key Takeaway : Concurrent features enable React to create more responsive
user interfaces by prioritizing urgent updates and allowing non-urgent work to
be interrupted. Use them to improve perceived performance and user experience.
Overview
React’s streaming and Suspense features enable progressive rendering and better loading experiences. These features are particularly powerful in Next.js applications where they can significantly improve perceived performance.
What is Streaming?
Streaming allows React to send HTML to the browser as it’s being rendered on the server, rather than waiting for the entire page to be ready.
Streaming Benefits Faster initial page load Progressive content rendering Better perceived performance Reduced time to first byte
Streaming Use Cases Slow data fetching Large component trees Third-party integrations Complex calculations
Understanding Suspense
Suspense is a React component that lets you declaratively handle loading states for components that are waiting for data.
Basic Suspense Usage
import { Suspense } from "react" ;
function App () {
return (
< div >
< h1 > My App </ h1 >
< Suspense fallback = {<div>Loading ...</ div > } >
< SlowComponent />
</ Suspense >
</ div >
);
}
Suspense with Custom Loading
import { Suspense } from "react" ;
import LoadingSpinner from "@/components/LoadingSpinner" ;
function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
< Suspense fallback = {<LoadingSpinner />} >
< DashboardContent />
</ Suspense >
</ div >
);
}
Streaming in Next.js
Automatic Streaming
Next.js automatically streams server components by default:
// app/dashboard/page.tsx
import { db } from "@/lib/database" ;
export default async function Dashboard () {
// This will stream as it loads
const stats = await db . getDashboardStats ();
return (
< div >
< h1 > Dashboard </ h1 >
< div className = "stats" >
{ stats . map (( stat ) => (
< div key = {stat. id } >
{ stat . title } : { stat . value }
</ div >
))}
</ div >
</ div >
);
}
Manual Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from "react" ;
import DashboardStats from "@/components/DashboardStats" ;
import DashboardCharts from "@/components/DashboardCharts" ;
export default function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
< Suspense fallback = {<StatsSkeleton />} >
< DashboardStats />
</ Suspense >
< Suspense fallback = {<ChartsSkeleton />} >
< DashboardCharts />
</ Suspense >
</ div >
);
}
Advanced Streaming Patterns
Nested Suspense Boundaries
// app/dashboard/page.tsx
import { Suspense } from "react" ;
export default function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
< Suspense fallback = {<div>Loading stats ...</ div > } >
< StatsSection />
</ Suspense >
< Suspense fallback = {<div>Loading charts ...</ div > } >
< ChartsSection />
</ Suspense >
< Suspense fallback = {<div>Loading recent activity ...</ div > } >
< RecentActivity />
</ Suspense >
</ div >
);
}
// Each section can have its own Suspense boundaries
function StatsSection () {
return (
< div className = "stats-section" >
< Suspense fallback = {<div>Loading user stats ...</ div > } >
< UserStats />
</ Suspense >
< Suspense fallback = {<div>Loading system stats ...</ div > } >
< SystemStats />
</ Suspense >
</ div >
);
}
Streaming with Error Boundaries
// app/dashboard/page.tsx
import { Suspense } from "react" ;
import ErrorBoundary from "@/components/ErrorBoundary" ;
export default function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
< ErrorBoundary fallback = {<StatsError />} >
< Suspense fallback = {<StatsSkeleton />} >
< DashboardStats />
</ Suspense >
</ ErrorBoundary >
< ErrorBoundary fallback = {<ChartsError />} >
< Suspense fallback = {<ChartsSkeleton />} >
< DashboardCharts />
</ Suspense >
</ ErrorBoundary >
</ div >
);
}
Data Fetching with Suspense
Server Components with Suspense
// app/blog/page.tsx
import { Suspense } from "react" ;
import { db } from "@/lib/database" ;
async function BlogPosts () {
const posts = await db . getBlogPosts ();
return (
< div className = "blog-posts" >
{ posts . map (( post ) => (
< article key = {post. id } >
< h2 >{post. title } </ h2 >
< p >{post. excerpt } </ p >
</ article >
))}
</ div >
);
}
async function BlogSidebar () {
const categories = await db . getCategories ();
return (
< aside className = "blog-sidebar" >
< h3 > Categories </ h3 >
< ul className = "list-disc list-inside" >
{ categories . map (( category ) => (
< li key = {category. id } >
< a href = { `/blog/category/ ${ category . slug } ` } > {category. name } </ a >
</ li >
))}
</ ul >
</ aside >
);
}
export default function BlogPage () {
return (
< div className = "blog-layout" >
< main >
< Suspense fallback = {<PostsSkeleton />} >
< BlogPosts />
</ Suspense >
</ main >
< aside >
< Suspense fallback = {<SidebarSkeleton />} >
< BlogSidebar />
</ Suspense >
</ aside >
</ div >
);
}
Client Components with Suspense
"use client" ;
import { Suspense , useState } from "react" ;
function UserProfile ({ userId } : { userId : string }) {
const [ user , setUser ] = useState ( null );
// Simulate data fetching
useState (() => {
fetch ( `/api/users/ ${ userId } ` )
. then (( res ) => res . json ())
. then ( setUser );
}, [ userId ]);
if ( ! user ) {
throw new Promise (( resolve ) => setTimeout ( resolve , 1000 ));
}
return (
< div className = "user-profile" >
< h2 >{user. name } </ h2 >
< p >{user. email } </ p >
</ div >
);
}
export default function ProfilePage () {
return (
< div >
< h1 > Profile </ h1 >
< Suspense fallback = {<ProfileSkeleton />} >
< UserProfile userId = "123" />
</ Suspense >
</ div >
);
}
Streaming Strategies
Critical Path First
Stream the most important content first
Progressive Enhancement
Load basic content first, then enhance
Parallel Loading
Load multiple sections simultaneously
Error Isolation
Use error boundaries to prevent cascading failures
Loading State Design
// components/StatsSkeleton.tsx
export default function StatsSkeleton () {
return (
< div className = "stats-skeleton" >
< div className = "grid grid-cols-1 md:grid-cols-4 gap-6" >
{ Array . from ({ length : 4 }). map (( _ , i ) => (
< div key = { i } className = "bg-white p-6 rounded-lg shadow-md" >
< div className = "h-4 bg-gray-200 rounded w-24 mb-2 animate-pulse" > </ div >
< div className = "h-8 bg-gray-200 rounded w-16 animate-pulse" > </ div >
</ div >
))}
</ div >
</ div >
);
}
Real-World Examples
E-commerce Product Page
// app/products/[id]/page.tsx
import { Suspense } from "react" ;
async function ProductInfo ({ productId } : { productId : string }) {
const product = await db . getProduct ( productId );
return (
< div className = "product-info" >
< h1 >{product. name } </ h1 >
< p className = "price" > $ {product. price } </ p >
< p >{product. description } </ p >
</ div >
);
}
async function ProductReviews ({ productId } : { productId : string }) {
const reviews = await db . getProductReviews ( productId );
return (
< div className = "product-reviews" >
< h2 > Reviews </ h2 >
{ reviews . map (( review ) => (
< div key = {review. id } className = "review" >
< h3 >{review. title } </ h3 >
< p >{review. content } </ p >
</ div >
))}
</ div >
);
}
async function RelatedProducts ({ productId } : { productId : string }) {
const related = await db . getRelatedProducts ( productId );
return (
< div className = "related-products" >
< h2 > Related Products </ h2 >
< div className = "grid grid-cols-4 gap-4" >
{ related . map (( product ) => (
< div key = {product. id } className = "product-card" >
< h3 >{product. name } </ h3 >
< p > $ {product. price } </ p >
</ div >
))}
</ div >
</ div >
);
}
export default function ProductPage ({ params } : { params : { id : string } }) {
return (
< div className = "product-page" >
< Suspense fallback = {<ProductInfoSkeleton />} >
< ProductInfo productId = {params. id } />
</ Suspense >
< div className = "product-details" >
< Suspense fallback = {<ReviewsSkeleton />} >
< ProductReviews productId = {params. id } />
</ Suspense >
< Suspense fallback = {<RelatedSkeleton />} >
< RelatedProducts productId = {params. id } />
</ Suspense >
</ div >
</ div >
);
}
Dashboard with Multiple Data Sources
// app/dashboard/page.tsx
import { Suspense } from "react" ;
async function UserStats () {
const stats = await db . getUserStats ();
return < StatsWidget data ={ stats } />;
}
async function SystemMetrics () {
const metrics = await db . getSystemMetrics ();
return < MetricsWidget data ={ metrics } />;
}
async function RecentActivity () {
const activity = await db . getRecentActivity ();
return < ActivityFeed data ={ activity } />;
}
export default function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
< div className = "dashboard-grid" >
< div className = "stats-section" >
< Suspense fallback = {<StatsSkeleton />} >
< UserStats />
</ Suspense >
</ div >
< div className = "metrics-section" >
< Suspense fallback = {<MetricsSkeleton />} >
< SystemMetrics />
</ Suspense >
</ div >
< div className = "activity-section" >
< Suspense fallback = {<ActivitySkeleton />} >
< RecentActivity />
</ Suspense >
</ div >
</ div >
</ div >
);
}
Best Practices
Streaming Best Practices
Identify Slow Components
Find components that take time to load
Create Loading States
Design appropriate loading skeletons
Use Error Boundaries
Handle errors gracefully in streaming
Monitor Performance
Track streaming performance metrics
Suspense Best Practices
Granular Boundaries Use multiple Suspense boundaries Isolate slow components Provide specific loading states
Error Handling Combine with error boundaries Provide fallback content Handle network failures
Common Patterns
Progressive Loading
// app/dashboard/page.tsx
export default function Dashboard () {
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
{ /* Load immediately */ }
< QuickStats />
{ /* Load progressively */ }
< Suspense fallback = {<div>Loading detailed stats ...</ div > } >
< DetailedStats />
</ Suspense >
< Suspense fallback = {<div>Loading charts ...</ div > } >
< Charts />
</ Suspense >
</ div >
);
}
Conditional Streaming
// app/dashboard/page.tsx
import { Suspense } from "react" ;
export default function Dashboard ({
searchParams ,
} : {
searchParams : { tab ?: string };
}) {
const activeTab = searchParams . tab || "overview" ;
return (
< div className = "dashboard" >
< h1 > Dashboard </ h1 >
{ activeTab === " overview " && (
< Suspense fallback = {<OverviewSkeleton />} >
< OverviewTab />
</ Suspense >
)}
{ activeTab === " analytics " && (
< Suspense fallback = {<AnalyticsSkeleton />} >
< AnalyticsTab />
</ Suspense >
)}
</ div >
);
}
Troubleshooting
Problem : Content not streaming as expected
Solution : Check that components are server components and data fetching is properly implemented
Problem : Suspense fallback not showing Solution : Ensure the component
is actually suspending (throwing a promise)
Problem : Loading states appear too briefly Solution : Add minimum
loading time or optimize data fetching
Problem : Errors breaking the streaming experience
Solution : Wrap Suspense boundaries with error boundaries
Key Takeaway : Streaming and Suspense are powerful tools for creating
better user experiences. Use them to progressively load content and provide
immediate feedback to users while data is being fetched.