Modern React 18+ Features REACT 18+
1. useTransition Hook for Non-blocking Updates NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useTransition Hook | const [isPending, startTransition] = useTransition() |
Mark state updates as non-urgent transitions | Keep UI responsive during updates |
| startTransition | startTransition(() => {}) |
Wrap state updates to mark as transition | Heavy computations, filtering |
| isPending Flag | isPending ? 'Loading' : 'Done' |
Boolean indicating transition in progress | Show loading indicators |
| Priority System | Urgent vs transition updates | React prioritizes urgent updates first | Input responsiveness |
| Interruptible Renders | React can pause transitions | Allow urgent updates to interrupt | Smooth user experience |
Example: useTransition for responsive filtering
import { useState, useTransition } from 'react';
const SearchableList = ({ items }) => {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
// Urgent: Update input immediately
setQuery(value);
// Non-urgent: Filter in background
startTransition(() => {
const filtered = items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending ? (
<div>Updating results...</div>
) : (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
};
// Tab switching example
const TabContainer = () => {
const [activeTab, setActiveTab] = useState('tab1');
const [isPending, startTransition] = useTransition();
const switchTab = (tab) => {
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<div className={isPending ? 'loading' : ''}>
<button onClick={() => switchTab('tab1')}>Tab 1</button>
<button onClick={() => switchTab('tab2')}>Tab 2</button>
<button onClick={() => switchTab('tab3')}>Tab 3</button>
</div>
<TabContent tab={activeTab} />
</div>
);
};
Note: useTransition enables concurrent rendering. Transition
updates can be interrupted by more urgent updates, keeping the UI responsive even during expensive operations.
2. useDeferredValue Hook for Deferred Values NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useDeferredValue | const deferred = useDeferredValue(value) |
Defer updating a value to keep UI responsive | Expensive child component updates |
| Deferred Rendering | <Component value={deferred} /> |
Pass deferred value to expensive component | Debounce-like behavior |
| Initial Value | useDeferredValue(value, initialValue) |
Optional initial value during SSR | Server-side rendering |
| Value Comparison | value !== deferred |
Check if deferral is in progress | Show loading states |
Example: useDeferredValue for responsive search
import { useState, useDeferredValue, memo } from 'react';
const SearchResults = ({ query }) => {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ExpensiveList query={deferredQuery} />
</div>
);
};
// Expensive component that benefits from deferral
const ExpensiveList = memo(({ query }) => {
const items = useMemo(() => {
// Expensive filtering/computation
return largeDataset.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
// Complete search component
const SearchPage = () => {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isSearching = query !== deferredQuery;
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isSearching && <span>Searching...</span>}
<SearchResults query={deferredQuery} />
</div>
);
};
// Comparison: useTransition vs useDeferredValue
// useTransition: You control the state update
// useDeferredValue: React controls when to update the value
Note: Use useDeferredValue when you can't control the state
update (props from parent). Use useTransition when you control the state update.
3. useId Hook for Unique Identifiers NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useId Hook | const id = useId() |
Generate unique ID for accessibility attributes | Form labels, ARIA attributes |
| SSR Safe | Same ID on client and server | Consistent IDs across hydration | Avoid hydration mismatches |
| Multiple IDs | id + '-label', id + '-error' |
Derive multiple IDs from single hook | Related elements |
| List Items | Don't use for key prop | Keys should be from data, not generated | Use stable data IDs |
Example: useId for accessible form fields
import { useId } from 'react';
// Basic form field with unique ID
const TextField = ({ label, error }) => {
const id = useId();
const errorId = id + '-error';
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
type="text"
aria-describedby={error ? errorId : undefined}
aria-invalid={error ? 'true' : 'false'}
/>
{error && (
<span id={errorId} role="alert">
{error}
</span>
)}
</div>
);
};
// Complex form with multiple fields
const ContactForm = () => {
const id = useId();
return (
<form>
<div>
<label htmlFor={id + '-name'}>Name</label>
<input id={id + '-name'} type="text" />
</div>
<div>
<label htmlFor={id + '-email'}>Email</label>
<input id={id + '-email'} type="email" />
</div>
<div>
<label htmlFor={id + '-message'}>Message</label>
<textarea id={id + '-message'} />
</div>
</form>
);
};
// Reusable input component
const Input = ({ label, type = 'text', ...props }) => {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} {...props} />
</>
);
};
// ARIA example
const Accordion = ({ title, children }) => {
const [isOpen, setIsOpen] = useState(false);
const id = useId();
const headerId = id + '-header';
const panelId = id + '-panel';
return (
<div>
<button
id={headerId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen(!isOpen)}
>
{title}
</button>
<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!isOpen}
>
{children}
</div>
</div>
);
};
Warning: Don't use
useId to generate keys in a list. Keys should come from your
data. Use useId only for accessibility attributes like id, htmlFor,
aria-*.
4. useSyncExternalStore Hook for External State NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useSyncExternalStore | useSyncExternalStore(subscribe, getSnapshot) |
Subscribe to external store | Redux, Zustand, browser APIs |
| Subscribe Function | subscribe(callback) |
Subscribe to store changes | Return unsubscribe function |
| getSnapshot Function | getSnapshot() |
Get current store value | Must return immutable value |
| SSR getSnapshot | useSyncExternalStore(..., getServerSnapshot) |
Server-side initial value | Hydration consistency |
| Tearing Prevention | Consistent state across components | No visual inconsistencies | Concurrent rendering safety |
Example: useSyncExternalStore for browser APIs
import { useSyncExternalStore } from 'react';
// Browser online status
const useOnlineStatus = () => {
return useSyncExternalStore(
// Subscribe function
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
// getSnapshot function
() => navigator.onLine,
// getServerSnapshot (SSR)
() => true
);
};
// Usage
const StatusIndicator = () => {
const isOnline = useOnlineStatus();
return (
<div>
{isOnline ? '🟢 Online' : '🔴 Offline'}
</div>
);
};
// Window width hook
const useWindowWidth = () => {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => window.innerWidth,
() => 0 // SSR default
);
};
// Custom store example
class CountStore {
constructor() {
this.count = 0;
this.listeners = new Set();
}
subscribe = (callback) => {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
};
getSnapshot = () => {
return this.count;
};
increment = () => {
this.count++;
this.listeners.forEach(listener => listener());
};
}
const store = new CountStore();
const useCountStore = () => {
return useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
};
const Counter = () => {
const count = useCountStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => store.increment()}>+</button>
</div>
);
};
// Local storage sync
const useLocalStorage = (key, initialValue) => {
return useSyncExternalStore(
(callback) => {
const handler = (e) => {
if (e.key === key) callback();
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
},
() => {
try {
return JSON.parse(localStorage.getItem(key)) ?? initialValue;
} catch {
return initialValue;
}
},
() => initialValue
);
};
Note: useSyncExternalStore is primarily for library authors.
Most apps should use built-in hooks or state management libraries that already use this hook internally.
5. useInsertionEffect Hook for CSS-in-JS Libraries NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useInsertionEffect | useInsertionEffect(() => {}, [deps]) |
Insert styles before layout effects | CSS-in-JS libraries only |
| Timing | Before useLayoutEffect | Runs before DOM mutations are visible | Inject <style> tags |
| No DOM Access | Cannot read/write DOM | DOM not yet updated | Style injection only |
| Library Use Only | Not for application code | Advanced library feature | styled-components, emotion |
Example: useInsertionEffect for style injection
import { useInsertionEffect } from 'react';
// CSS-in-JS library example (simplified)
const styleCache = new Map();
const useStyleInjection = (css, id) => {
useInsertionEffect(() => {
// Check if style already injected
if (styleCache.has(id)) {
return;
}
// Create and inject style tag
const style = document.createElement('style');
style.textContent = css;
style.setAttribute('data-style-id', id);
document.head.appendChild(style);
// Cache it
styleCache.set(id, style);
// Cleanup
return () => {
document.head.removeChild(style);
styleCache.delete(id);
};
}, [css, id]);
};
// Example usage in a CSS-in-JS library
const useStyledComponent = (styles) => {
const id = useId();
const className = \`styled-\${id}\`;
const css = \`.styled-\${id} {
\${Object.entries(styles)
.map(([key, value]) => \`\${key}: \${value};\`)
.join('\n')}
}\`;
useStyleInjection(css, id);
return className;
};
// Usage in components
const MyComponent = () => {
const className = useStyledComponent({
'background-color': '#f0f0f0',
'padding': '20px',
'border-radius': '8px'
});
return <div className={className}>Styled Content</div>;
};
// Real-world pattern (styled-components-like)
const styled = (tag) => (styles) => {
return (props) => {
const id = useId();
const className = \`sc-\${id}\`;
useInsertionEffect(() => {
const css = generateCSS(className, styles, props);
injectStyle(css, id);
return () => removeStyle(id);
}, [className, styles, props]);
return React.createElement(tag, {
...props,
className: \`\${className} \${props.className || ''}\`
});
};
};
// Usage
const Button = styled('button')\`
background: blue;
color: white;
padding: 10px 20px;
\`;
// In component
<Button>Click Me</Button>
Warning:
useInsertionEffect is ONLY for CSS-in-JS library authors. Regular
applications should never use this hook. Use useEffect or useLayoutEffect instead.
6. Concurrent Features and Automatic Batching NEW
| Feature | Description | Benefit | Example |
|---|---|---|---|
| Automatic Batching | Multiple state updates batched automatically | Fewer renders, better performance | Event handlers, timeouts, promises |
| Concurrent Rendering | React can interrupt rendering work | Keep UI responsive during updates | Large list updates |
| Transitions | Mark updates as non-urgent | Prioritize urgent updates | useTransition, startTransition |
| Streaming SSR | Send HTML in chunks as ready | Faster Time to First Byte | Suspense on server |
| Selective Hydration | Hydrate components as user interacts | Faster Time to Interactive | Priority-based hydration |
| Strict Mode Double Render | Effects run twice in development | Catch side effect bugs early | Development only |
Example: Automatic batching in React 18
import { useState } from 'react';
import { flushSync } from 'react-dom';
// React 18: Automatic batching everywhere
const AutoBatchingExample = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render'); // Only logs once per click
// Event handlers - batched (React 17 too)
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
// Only 1 render
};
// Timeouts - NOW batched (was 2 renders in React 17)
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// Only 1 render in React 18!
}, 1000);
};
// Promises - NOW batched (was 2 renders in React 17)
const handleAsync = async () => {
const data = await fetchData();
setCount(data.count);
setFlag(data.flag);
// Only 1 render in React 18!
};
// Native event handlers - NOW batched
useEffect(() => {
const handler = () => {
setCount(c => c + 1);
setFlag(f => !f);
// Only 1 render in React 18!
};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// Opt-out of batching with flushSync (rare)
const handleNoFlush = () => {
flushSync(() => {
setCount(c => c + 1);
});
// React renders here
flushSync(() => {
setFlag(f => !f);
});
// React renders again - 2 total renders
};
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Update</button>
<button onClick={handleTimeout}>Update (Timeout)</button>
<button onClick={handleAsync}>Update (Async)</button>
</div>
);
};
// Concurrent features example
const ConcurrentExample = () => {
const [resource, setResource] = useState(initialResource);
// Wrap in transition for smooth updates
const handleUpdate = () => {
startTransition(() => {
setResource(fetchNewResource());
});
};
return (
<Suspense fallback={<Spinner />}>
<button onClick={handleUpdate}>Update</button>
<ResourceComponent resource={resource} />
</Suspense>
);
};
// Strict Mode double render
const StrictModeExample = () => {
useEffect(() => {
console.log('Effect');
// In development, logs twice to help find bugs
return () => {
console.log('Cleanup');
// Also runs twice in development
};
}, []);
return <div>Component</div>;
};
React 18 Key Improvements
- Automatic Batching: Multiple state updates batched everywhere (promises, timeouts, native events)
- Concurrent Rendering: React can pause and resume work, keeping UI responsive
- useTransition: Mark updates as non-urgent for better UX
- useDeferredValue: Defer expensive updates while keeping input responsive
- Streaming SSR: Send HTML in chunks for faster initial load
- Selective Hydration: Prioritize hydration based on user interaction
Note: React 18's concurrent features are opt-in. Use createRoot
instead of render to enable them:
ReactDOM.createRoot(container).render(<App />)