React 18: New Features and Concurrent Rendering
React 18: New Features and Concurrent Rendering
React 18 introduces concurrent features that fundamentally change how React renders your applications. These features enable more responsive user interfaces by allowing React to prepare multiple versions of the UI simultaneously.
Upgrading to React 18
npm install react react-dom
Enabling Concurrent Features
// Before (React 17)
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))
// After (React 18)
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)
Automatic Batching
React 18 automatically batches state updates, even in promises, setTimeout, and native event handlers.
Before React 18
// This would cause 2 re-renders in React 17
async function handleClick() {
await fetchData()
setData(data) // Re-render 1
setLoading(false) // Re-render 2
}
// Also in setTimeout
setTimeout(() => {
setCount(1) // Re-render 1
setFlag(true) // Re-render 2
}, 1000)
In React 18
// Now automatically batched - only 1 re-render!
async function handleClick() {
await fetchData()
setData(data) //
setLoading(false) // Batched together
}
// Also works in setTimeout
setTimeout(() => {
setCount(1) //
setFlag(true) // Batched together
}, 1000)
// And native events
element.addEventListener('click', () => {
setCount(1) //
setFlag(true) // Only 1 re-render
})
Opting Out of Batching
Use flushSync when you need to force immediate updates:
import { flushSync } from 'react-dom'
function handleClick() {
flushSync(() => {
setCount(1) // Immediately re-renders
})
setFlag(true) // Another re-render
}
Transitions
Transitions let you mark certain updates as non-urgent, keeping the UI responsive during heavy operations.
useTransition Hook
import { useTransition, useState } from 'react'
function SearchComponent() {
const [isPending, startTransition] = useTransition()
const [searchTerm, setSearchTerm] = useState('')
const [results, setResults] = useState([])
const handleSearch = (e) => {
const value = e.target.value
// Urgent: Update input immediately
setSearchTerm(value)
// Non-urgent: Mark expensive operation as transition
startTransition(() => {
const filtered = largeDataArray.filter(item =>
item.name.includes(value)
)
setResults(filtered)
})
}
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleSearch}
/>
{isPending && <span>Loading...</span>}
<Results items={results} />
</div>
)
}
useDeferredValue
useDeferredValue creates a deferred version of a value that lags behind updates:
import { useDeferredValue, useMemo, useState } from 'react'
function SearchResults({ query, data }) {
// Deferred value lags behind query
const deferredQuery = useDeferredValue(query)
// Expensive computation runs against deferred value
const filteredItems = useMemo(() => {
return data.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
)
}, [deferredQuery, data])
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
Suspense for Data Fetching
React 18 enhances Suspense to work with data fetching, not just code splitting.
Basic Suspense Pattern
import { Suspense } from 'react'
function App() {
return (
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
)
}
Using Suspense with Data Fetching
// api.js - Using a simple Suspense-compatible resource
function fetchData(url) {
let status = 'pending'
let result
let suspender = fetch(url)
.then(r => r.json())
.then(data => {
status = 'success'
result = data
})
.catch(error => {
status = 'error'
result = error
})
return {
read() {
if (status === 'pending') throw suspender
if (status === 'error') throw result
return result
}
}
}
// Component.js
const resource = fetchData('/api/users')
function Users() {
const users = resource.read() // Suspends until data is ready
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
// App.js
function App() {
return (
<Suspense fallback={<Spinner />}>
<Users />
</Suspense>
)
}
Suspense with Transitions
import { Suspense, useState, useTransition } from 'react'
function App() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState('home')
const selectTab = (nextTab) => {
startTransition(() => {
setTab(nextTab)
})
}
return (
<div>
<TabBar onSelect={selectTab} />
{isPending && <Spinner />}
<Suspense fallback={<Loading />}>
<TabContent tab={tab} />
</Suspense>
</div>
)
}
New Hooks
useId
Generate unique IDs for accessibility attributes:
import { useId } from 'react'
function FormField({ label }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
// Multiple IDs
function Checkbox() {
const id = useId()
return (
<>
<label htmlFor={`${id}-checkbox`}>Accept terms</label>
<input id={`${id}-checkbox`} type="checkbox" />
</>
)
}
useSyncExternalStore
Synchronize React state with external stores:
import { useSyncExternalStore } from 'react'
// Subscribe to browser APIs
function useOnlineStatus() {
return useSyncExternalStore(
// Subscribe function
(callback) => {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
},
// Get snapshot
() => navigator.onLine,
// Server snapshot
() => true
)
}
function StatusIndicator() {
const isOnline = useOnlineStatus()
return <span>{isOnline ? 'Online' : 'Offline'}</span>
}
useInsertionEffect
For CSS-in-JS libraries to inject styles:
import { useInsertionEffect } from 'react'
function useStyle(rule) {
useInsertionEffect(() => {
const style = document.createElement('style')
style.textContent = rule
document.head.appendChild(style)
return () => document.head.removeChild(style)
}, [rule])
}
Streaming Server-Side Rendering
React 18 enables streaming HTML with selective hydration.
Using with Node.js Streams
// server.js
import { renderToPipeableStream } from 'react-dom/server'
import App from './App'
app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
res.setHeader('Content-Type', 'text/html')
pipe(res)
},
})
})
Suspense in SSR
// App.js
function App() {
return (
<html>
<head>
<title>My App</title>
</head>
<body>
<Navbar />
<Suspense fallback={<ArticleSkeleton />}>
<Article />
</Suspense>
<Footer />
</body>
</html>
)
}
Strict Mode Changes
React 18's Strict Mode simulates mounting, unmounting, and remounting components to help find side effects:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
)
Handling Double Effects
function Component() {
useEffect(() => {
// This runs twice in Strict Mode
console.log('Mount')
return () => {
console.log('Unmount')
}
}, [])
// Fix: Clean up properly
useEffect(() => {
const controller = new AbortController()
fetchData({ signal: controller.signal })
return () => {
controller.abort()
}
}, [])
}
Performance Best Practices
1. Use Transitions for Expensive Updates
function List({ items }) {
const [filter, setFilter] = useState('')
const [isPending, startTransition] = useTransition()
const handleFilter = (value) => {
// Immediate UI update
setFilter(value)
// Defer expensive filtering
startTransition(() => {
setFilteredItems(filterItems(items, value))
})
}
return (
<div>
<input onChange={(e) => handleFilter(e.target.value)} />
{isPending ? <Loading /> : <Items items={filteredItems} />}
</div>
)
}
2. Memoize Deferred Computations
import { useDeferredValue, useMemo } from 'react'
function Search({ query, items }) {
const deferredQuery = useDeferredValue(query)
const results = useMemo(() => {
return heavySearch(items, deferredQuery)
}, [deferredQuery, items])
return <Results results={results} />
}
3. Use Suspense Boundaries Strategically
function App() {
return (
<Layout>
<Suspense fallback={<NavSkeleton />}>
<Navigation />
</Suspense>
<main>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</main>
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</Layout>
)
}
Conclusion
React 18's concurrent features represent a significant evolution in how React handles rendering. By leveraging automatic batching, transitions, and Suspense, you can build more responsive and user-friendly applications.
Migration Checklist
- Upgrade to React 18 and update
rendertocreateRoot - Review effects for proper cleanup (Strict Mode tests this)
- Identify opportunities for
useTransitionin slow interactions - Consider
useDeferredValuefor type-ahead scenarios - Use
useIdfor stable unique IDs
React 18 opens up exciting possibilities for building faster, more responsive user interfaces. Start exploring these features today!