React 18+ Concurrent Features and State
1. useTransition Hook for Non-blocking State Updates
| Feature | API | Description | Use Case |
|---|---|---|---|
| useTransition | const [isPending, startTransition] = useTransition() |
Mark state updates as non-urgent transitions | Keep UI responsive during heavy updates (search, filtering, tabs) |
| isPending | boolean |
Indicates if transition is in progress | Show loading indicators during transition |
| startTransition | startTransition(() => setState(...)) |
Wrap low-priority state updates | Allow urgent updates (typing) to interrupt slow updates (filtering) |
| Priority System | Urgent vs Transition updates | React prioritizes urgent updates over transitions | Input responsiveness remains high during background work |
| Interruptible | Transitions can be abandoned | New urgent updates cancel ongoing transitions | No stale results when user types quickly |
| No Suspense Needed | Works without Suspense boundaries | Simpler than Suspense for CPU-bound work | Heavy computations, large list rendering |
| Batching | All updates in startTransition batched | Single re-render for multiple state updates | Performance optimization for complex updates |
| Concurrent Rendering | Enables React 18 concurrent features | Render can be paused and resumed | Smooth UI even with expensive renders |
Example: Search with transitions for responsive input
import { useState, useTransition } from 'react';
function SearchList({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
// Urgent update - input stays responsive
setQuery(value);
// Non-urgent update - can be interrupted
startTransition(() => {
// Heavy filtering operation
const filtered = items.filter(item =>
item.title.toLowerCase().includes(value.toLowerCase()) ||
item.description.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending && <span>Searching...</span>}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{filteredItems.map(item => (
<ItemCard key={item.id} item={item} />
))}
</div>
</div>
);
}
// Without useTransition - input becomes sluggish
function SlowSearch({ items }) {
const [query, setQuery] = useState('');
// Both updates are urgent - blocks input
const filteredItems = items.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
return <div>...</div>;
}
Example: Tab switching with smooth transitions
import { useState, useTransition } from 'react';
function TabContainer() {
const [activeTab, setActiveTab] = useState('about');
const [isPending, startTransition] = useTransition();
const switchTab = (tab) => {
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<div className="tabs">
{['about', 'posts', 'contact'].map(tab => (
<button
key={tab}
onClick={() => switchTab(tab)}
className={activeTab === tab ? 'active' : ''}
disabled={isPending}
>
{tab}
</button>
))}
</div>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{activeTab === 'about' && <AboutTab />}
{activeTab === 'posts' && <PostsTab />} {/* Expensive */}
{activeTab === 'contact' && <ContactTab />}
</div>
</div>
);
}
// Complex tab that benefits from transition
function PostsTab() {
// Expensive rendering - thousands of items
const posts = useMemo(() =>
generatePosts(10000), []
);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Note: useTransition keeps input responsive by deprioritizing expensive updates. Use for
filtering, searching, tab switching, or any CPU-intensive state changes. The isPending flag lets you show
loading states or reduce opacity during transitions.
2. useDeferredValue for State Value Deferring
| Feature | API | Description | Difference from useTransition |
|---|---|---|---|
| useDeferredValue | const deferredValue = useDeferredValue(value) |
Defer rendering with stale value | For values you don't control (props), useTransition for state you own |
| Debouncing Effect | Shows previous value during update | Similar to debouncing but integrated with React | Automatic unlike manual setTimeout debouncing |
| Memoization | Combine with useMemo/memo | Prevent re-render of expensive components | Deferred value triggers re-render only when ready |
| No isPending | No loading state flag | Compare value !== deferredValue for pending | useTransition provides isPending flag |
| Background Update | Deferred update happens in background | UI stays responsive during update | Similar priority to startTransition |
| Initial Render | First render uses actual value | Only subsequent updates deferred | No delay on initial mount |
| Cancellation | New value cancels pending deferred update | Prevents stale results | Interruptible like transitions |
| Use Cases | Expensive computations, large lists | When you receive value from parent | useTransition when you control the state |
Example: Deferred search results
import { useState, useDeferredValue, memo } from 'react';
function SearchApp() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Check if deferred value is stale
const isStale = query !== deferredQuery;
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{/* Input updates immediately */}
<div>Searching for: {query}</div>
{/* Results update with delay */}
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}
// Expensive component that benefits from deferred value
const SearchResults = memo(function SearchResults({ query }) {
const items = useMemo(() => {
// Expensive filtering/search operation
return hugeList.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
return (
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
});
Example: Deferred value vs regular value comparison
import { useState, useDeferredValue, memo } from 'react';
function SliderDemo() {
const [value, setValue] = useState(0);
const deferredValue = useDeferredValue(value);
return (
<div>
<input
type="range"
min="0"
max="100"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
/>
<div>
<div>Current: {value}</div>
<div>Deferred: {deferredValue}</div>
</div>
{/* Slider updates immediately */}
<FastComponent value={value} />
{/* Chart updates with delay, stays responsive */}
<SlowChart value={deferredValue} />
</div>
);
}
const SlowChart = memo(function SlowChart({ value }) {
// Simulate expensive render
const chartData = useMemo(() => {
const data = [];
for (let i = 0; i < 10000; i++) {
data.push(Math.sin(i / 100) * value);
}
return data;
}, [value]);
return <Canvas data={chartData} />;
});
// Without useDeferredValue - slider becomes laggy
function LaggySlider() {
const [value, setValue] = useState(0);
return (
<div>
<input
type="range"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
/>
{/* Both update together - slider lags */}
<SlowChart value={value} />
</div>
);
}
Note: useDeferredValue is ideal when you receive a frequently changing value from props. Use
with memo() to prevent re-renders until deferred value updates. For state you control directly, prefer
useTransition with startTransition.
3. Concurrent Rendering and State Consistency
| Concept | Description | Implications | Best Practices |
|---|---|---|---|
| Concurrent Rendering | React can pause, resume, or abandon renders | Renders may happen multiple times before commit | Render functions must be pure, no side effects |
| Tearing | Different parts showing different state snapshots | Can occur with external stores in concurrent mode | Use useSyncExternalStore to prevent tearing |
| State Snapshots | Each render gets consistent state snapshot | State doesn't change mid-render | Rely on this for consistent derived values |
| Automatic Batching | All state updates batched (even in async) | Fewer re-renders in React 18 | No manual batching needed in most cases |
| flushSync | flushSync(() => setState(...)) |
Force synchronous update (opt-out batching) | Use sparingly, only when absolutely needed |
| Strict Mode | Double-invokes functions in development | Catches impure render functions | Keep renders pure, no mutations or side effects |
| Time Slicing | Long renders split into chunks | Browser stays responsive during heavy work | Enabled automatically with transitions |
| Work Prioritization | High priority work interrupts low priority | User interactions stay responsive | Mark background updates as transitions |
Example: Automatic batching in React 18
import { useState } from 'react';
import { flushSync } from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render count:', count, 'flag:', flag);
// React 18: Single re-render (batched)
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
// Only ONE render, not two
};
// React 18: Even async updates are batched
const handleAsync = async () => {
await fetch('/api/data');
setCount(c => c + 1);
setFlag(f => !f);
// Still only ONE render!
};
// React 18: Timeouts also batched
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// Still batched!
}, 1000);
};
// Opt-out of batching with flushSync (rarely needed)
const handleFlushSync = () => {
flushSync(() => {
setCount(c => c + 1); // Render immediately
});
setFlag(f => !f); // Second render
// TWO renders
};
return (
<div>
<p>Count: {count}, Flag: {flag ? 'true' : 'false'}</p>
<button onClick={handleClick}>Sync Update</button>
<button onClick={handleAsync}>Async Update</button>
<button onClick={handleTimeout}>Timeout Update</button>
<button onClick={handleFlushSync}>FlushSync (not batched)</button>
</div>
);
}
Example: Pure render functions for concurrent mode
import { useState } from 'react';
// ❌ IMPURE - Will cause issues in concurrent mode
let renderCount = 0;
function ImpureComponent() {
renderCount++; // Side effect during render!
const [state, setState] = useState(0);
// Double counting in StrictMode/concurrent renders
return <div>Renders: {renderCount}</div>;
}
// ✅ PURE - Safe for concurrent mode
function PureComponent() {
const [state, setState] = useState(0);
const [renderCount, setRenderCount] = useState(0);
// Track renders in effect, not during render
useEffect(() => {
setRenderCount(c => c + 1);
});
return <div>Renders: {renderCount}</div>;
}
// ❌ IMPURE - Mutating external object
const cache = { data: null };
function MutatingComponent({ id }) {
cache.data = fetchData(id); // Mutation during render!
return <div>{cache.data}</div>;
}
// ✅ PURE - Use state or refs for caching
function NonMutatingComponent({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(id).then(setData);
}, [id]);
return <div>{data}</div>;
}
// ✅ PURE - Derived values calculated during render
function DerivedComponent({ items }) {
// Pure computation - no side effects
const total = items.reduce((sum, item) => sum + item.price, 0);
const average = total / items.length;
return <div>Average: {average}</div>;
}
Warning: Concurrent rendering can call render functions multiple times before committing. Keep
renders pure - no mutations, no side effects, no external state changes. Use useEffect for side effects, not
render phase.
4. useSyncExternalStore for External State Integration
| Feature | API | Description | Purpose |
|---|---|---|---|
| useSyncExternalStore | useSyncExternalStore(subscribe, getSnapshot) |
Subscribe to external stores safely | Prevent tearing in concurrent mode |
| subscribe | (callback) => unsubscribe |
Register listener for store changes | React re-renders when store updates |
| getSnapshot | () => currentValue |
Return current store value | Must return same value for same store state |
| getServerSnapshot | () => serverValue |
Optional server-side value | SSR hydration without mismatch |
| Tearing Prevention | Ensures consistent state across tree | All components see same snapshot | Critical for concurrent rendering |
| External Stores | Redux, Zustand, custom stores, browser APIs | Any non-React state source | Safe integration with React 18 |
| Selector Function | getSnapshot: () => store.getState().slice |
Select specific state slice | Optimize re-renders, only subscribe to needed data |
| Memoization | getSnapshot must be stable or memoized | Prevent infinite loops | Return same reference for same value |
Example: Custom store with useSyncExternalStore
import { useSyncExternalStore } from 'react';
// External store (not React state)
const createStore = (initialState) => {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (newState) => {
state = newState;
listeners.forEach(listener => listener());
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
};
const counterStore = createStore(0);
// Hook to use external store
function useCounter() {
const count = useSyncExternalStore(
counterStore.subscribe,
counterStore.getState
);
return count;
}
// Component usage
function Counter() {
const count = useCounter();
return (
<div>
<h1>{count}</h1>
<button onClick={() => counterStore.setState(count + 1)}>
Increment
</button>
</div>
);
}
// Multiple components stay in sync
function AnotherCounter() {
const count = useCounter();
return <div>Count: {count}</div>;
}
Example: Browser API integration with selectors
import { useSyncExternalStore } from 'react';
// Online status hook
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine,
() => true // Server snapshot
);
return isOnline;
}
// Window size hook
function useWindowSize() {
const size = useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => ({
width: window.innerWidth,
height: window.innerHeight
}),
() => ({ width: 0, height: 0 }) // Server
);
return size;
}
// Media query hook
function useMediaQuery(query) {
const matches = useSyncExternalStore(
(callback) => {
const mediaQuery = window.matchMedia(query);
mediaQuery.addEventListener('change', callback);
return () => mediaQuery.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false // Server
);
return matches;
}
// Component usage
function ResponsiveComponent() {
const isOnline = useOnlineStatus();
const { width } = useWindowSize();
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<div>
<div>Status: {isOnline ? 'Online' : 'Offline'}</div>
<div>Width: {width}px</div>
<div>Mobile: {isMobile ? 'Yes' : 'No'}</div>
</div>
);
}
Example: Redux integration with selectors
import { useSyncExternalStore } from 'react';
// Custom hook for Redux with selector
function useSelector(selector) {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState()) // Server
);
}
// Component usage
function UserProfile() {
const user = useSelector(state => state.user);
const theme = useSelector(state => state.ui.theme);
return (
<div className={theme}>
<h1>{user.name}</h1>
</div>
);
}
// Memoized selector to prevent infinite loops
function TodoList() {
const selectTodos = useMemo(
() => (state) => state.todos.filter(t => !t.completed),
[]
);
const activeTodos = useSelector(selectTodos);
return (
<ul>
{activeTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Note: useSyncExternalStore is essential for library authors and when integrating external
stores (Redux, browser APIs). It prevents tearing in concurrent mode by ensuring all components read the same
snapshot. Most app developers won't use it directly.
5. State Prioritization and Update Scheduling
| Priority Level | Type | Examples | Behavior |
|---|---|---|---|
| Urgent/Discrete | User interactions, controlled inputs | Clicks, typing, hover, focus | Executed immediately, highest priority |
| Transition | Non-urgent UI updates | Search results, filtering, navigation | Can be interrupted, lower priority |
| Deferred | Background updates | Analytics, logging, prefetch | Lowest priority, runs when idle |
| Lane System | Internal React priority mechanism | 32 priority lanes for scheduling | Fine-grained control of update order |
| Interruption | Higher priority cancels lower | Typing cancels pending search update | Ensures UI responsiveness |
| Batching | Same priority updates grouped | Multiple setState in handler | Single re-render per batch |
| Starvation Prevention | Low priority eventually executes | Transitions complete when idle | No infinite deferral |
| Time Slicing | Split work into chunks | Render 1000s of items without blocking | Browser remains responsive |
Example: Priority comparison in action
import { useState, useTransition, startTransition } from 'react';
function PriorityDemo() {
const [urgentCount, setUrgentCount] = useState(0);
const [transitionCount, setTransitionCount] = useState(0);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
// Urgent update - executes immediately
setUrgentCount(c => c + 1);
// Transition update - can be interrupted
startTransition(() => {
setTransitionCount(c => c + 1);
});
// If user clicks rapidly, urgent updates always go through
// but transition updates may be skipped/batched
};
return (
<div>
<button onClick={handleClick}>Increment</button>
{/* Always up-to-date */}
<div>Urgent: {urgentCount}</div>
{/* May lag behind during rapid clicks */}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
Transition: {transitionCount}
</div>
</div>
);
}
// Practical example: Search with priority
function SearchWithPriority() {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (value) => {
// Urgent: Input stays responsive
setInput(value);
// Transition: Expensive search update
startTransition(() => {
setQuery(value);
});
};
return (
<div>
{/* Always responsive */}
<input
value={input}
onChange={(e) => handleChange(e.target.value)}
/>
{/* Updates in background */}
<ExpensiveSearchResults query={query} isPending={isPending} />
</div>
);
}
Example: Multiple priority levels in complex UI
import { useState, useTransition, useEffect } from 'react';
function Dashboard() {
// Urgent state - user interactions
const [selectedTab, setSelectedTab] = useState('overview');
const [sidebarOpen, setSidebarOpen] = useState(true);
// Transition state - heavy renders
const [chartData, setChartData] = useState([]);
const [tableData, setTableData] = useState([]);
const [isPending, startTransition] = useTransition();
// Deferred/background state - analytics
const [analytics, setAnalytics] = useState({});
const switchTab = (tab) => {
// Urgent: Tab highlights immediately
setSelectedTab(tab);
// Transition: Heavy data processing
startTransition(() => {
const processed = processHeavyData(tab);
setChartData(processed.charts);
setTableData(processed.tables);
});
// Deferred: Track analytics (lowest priority)
setTimeout(() => {
setAnalytics(prev => ({
...prev,
[tab]: (prev[tab] || 0) + 1
}));
}, 0);
};
return (
<div>
{/* Urgent updates - always instant */}
<Tabs
active={selectedTab}
onSelect={switchTab}
/>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
Toggle Sidebar
</button>
{/* Transition updates - may be delayed */}
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Charts data={chartData} />
<Table data={tableData} />
</div>
{/* Background analytics - invisible to user */}
<AnalyticsTracker data={analytics} />
</div>
);
}
Note: React 18's priority system ensures user interactions always feel responsive. Mark
expensive updates as transitions, keep direct user feedback (clicks, typing) as urgent. The scheduler handles
the rest automatically.
6. Streaming SSR and State Hydration
| Feature | Description | Benefits | Considerations |
|---|---|---|---|
| Streaming SSR | Send HTML in chunks as components render | Faster TTFB, progressive page load | Requires React 18 + supporting framework |
| Selective Hydration | Hydrate components on-demand | Interactive sooner, prioritize visible content | User interactions trigger hydration |
| Suspense SSR | <Suspense> works on server |
Stream fallback, replace when ready | Slow components don't block entire page |
| Progressive Hydration | Hydrate critical parts first | Above-fold content interactive immediately | Below-fold waits until needed |
| State Serialization | Serialize server state to HTML | Client picks up where server left off | Avoid hydration mismatches |
| Hydration Mismatch | Server/client HTML differs | Console warnings, potential bugs | Use same data, avoid client-only code in render |
| useId | const id = useId() |
Generate stable IDs for SSR | Prevents mismatch from random IDs |
| Partial Hydration | Some components remain static | Save JS bundle size and hydration time | Mark non-interactive content as static |
Example: Streaming SSR with Suspense boundaries
import { Suspense } from 'react';
// Server-side rendering with streaming
function App() {
return (
<html>
<body>
{/* Critical content - renders first */}
<Header />
<Navigation />
{/* Slow component - streams later */}
<Suspense fallback={<SkeletonComments />}>
<Comments /> {/* Fetches data on server */}
</Suspense>
{/* Another slow component */}
<Suspense fallback={<SkeletonRecommendations />}>
<Recommendations />
</Suspense>
<Footer />
</body>
</html>
);
}
// Component with async data fetching
async function Comments() {
const comments = await fetchComments(); // Suspends
return (
<div>
{comments.map(c => (
<Comment key={c.id} data={c} />
))}
</div>
);
}
// Streaming sequence:
// 1. Server sends: Header, Nav, SkeletonComments, SkeletonRecs, Footer
// 2. Page visible immediately with skeletons
// 3. Comments finish -> streamed, replace skeleton
// 4. Recommendations finish -> streamed, replace skeleton
// 5. Progressive hydration makes interactive on-demand
Example: Avoiding hydration mismatches
import { useState, useEffect, useId } from 'react';
// ❌ WRONG - Hydration mismatch
function BadComponent() {
// Different on server vs client!
const timestamp = Date.now();
const random = Math.random();
return (
<div>
<div>Time: {timestamp}</div>
<div>Random: {random}</div>
</div>
);
}
// ✅ CORRECT - Same on server and client
function GoodComponent() {
const [timestamp, setTimestamp] = useState(null);
const [random, setRandom] = useState(null);
useEffect(() => {
// Client-only values set after hydration
setTimestamp(Date.now());
setRandom(Math.random());
}, []);
return (
<div>
{timestamp && <div>Time: {timestamp}</div>}
{random && <div>Random: {random}</div>}
</div>
);
}
// ❌ WRONG - Random ID causes mismatch
function BadFormField() {
const id = 'field-' + Math.random();
return (
<div>
<label htmlFor={id}>Name:</label>
<input id={id} />
</div>
);
}
// ✅ CORRECT - useId generates stable IDs
function GoodFormField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name:</label>
<input id={id} />
</div>
);
}
// ❌ WRONG - Conditional rendering based on client-only API
function BadResponsive() {
const isMobile = window.innerWidth < 768; // window undefined on server!
return isMobile ? <MobileView /> : <DesktopView />;
}
// ✅ CORRECT - Handle SSR gracefully
function GoodResponsive() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsMobile(window.innerWidth < 768);
const handler = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// Server renders desktop, hydrates correctly
return isMobile ? <MobileView /> : <DesktopView />;
}
Example: State serialization for SSR
// Server-side: Serialize initial state
import { renderToString } from 'react-dom/server';
async function handleRequest(req, res) {
const initialData = await fetchDataForPage(req.url);
const html = renderToString(<App initialData={initialData} />);
const fullHtml = `
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
res.send(fullHtml);
}
// Client-side: Hydrate with same data
import { hydrateRoot } from 'react-dom/client';
const initialData = window.__INITIAL_DATA__;
const root = document.getElementById('root');
hydrateRoot(root, <App initialData={initialData} />);
// Component uses serialized data
function App({ initialData }) {
const [data, setData] = useState(initialData);
// Both server and client render same initial content
return (
<div>
{data.items.map(item => (
<Item key={item.id} data={item} />
))}
</div>
);
}
Warning: Hydration mismatches cause React to discard server HTML and re-render on client
(expensive). Avoid Date.now(), Math.random(), window/document in render. Use useEffect for client-only code and
useId for stable IDs.
Section 16 Key Takeaways
- useTransition - Keep input responsive during expensive updates, mark non-urgent updates as transitions
- useDeferredValue - Defer expensive renders based on prop values, similar to debouncing
- Concurrent rendering - Renders can pause/resume, keep renders pure, no side effects
- useSyncExternalStore - Prevent tearing when integrating external stores in concurrent mode
- Priority system - Urgent updates (clicks, typing) interrupt transitions (searches, filtering)
- Streaming SSR - Progressive HTML delivery, selective hydration, avoid hydration mismatches