Back to Blog
ReactHooksuseDeferredValuePerformance

React useDeferredValue Hook Guide

Master the React useDeferredValue hook for deferring non-urgent updates and improving UI responsiveness.

B
Bootspring Team
Engineering
December 25, 2019
6 min read

The useDeferredValue hook defers updating part of the UI to keep the interface responsive. Here's how to use it.

Basic Usage#

1import { useState, useDeferredValue } from 'react'; 2 3function SearchResults() { 4 const [query, setQuery] = useState(''); 5 const deferredQuery = useDeferredValue(query); 6 7 // Input responds immediately 8 // Results update with deferred value 9 return ( 10 <div> 11 <input 12 value={query} 13 onChange={(e) => setQuery(e.target.value)} 14 placeholder="Search..." 15 /> 16 <Results query={deferredQuery} /> 17 </div> 18 ); 19} 20 21function Results({ query }) { 22 // Expensive computation or filtering 23 const results = searchItems(query); 24 25 return ( 26 <ul> 27 {results.map((item) => ( 28 <li key={item.id}>{item.name}</li> 29 ))} 30 </ul> 31 ); 32}

Showing Stale Content#

1import { useState, useDeferredValue, memo } from 'react'; 2 3function App() { 4 const [text, setText] = useState(''); 5 const deferredText = useDeferredValue(text); 6 7 // Check if showing stale content 8 const isStale = text !== deferredText; 9 10 return ( 11 <div> 12 <input 13 value={text} 14 onChange={(e) => setText(e.target.value)} 15 /> 16 17 <div style={{ opacity: isStale ? 0.5 : 1 }}> 18 <SlowList text={deferredText} /> 19 </div> 20 </div> 21 ); 22} 23 24// Memoize to prevent re-renders with same deferred value 25const SlowList = memo(function SlowList({ text }) { 26 const items = []; 27 for (let i = 0; i < 500; i++) { 28 items.push(<SlowItem key={i} text={text} />); 29 } 30 return <ul>{items}</ul>; 31}); 32 33function SlowItem({ text }) { 34 // Simulate slow render 35 const startTime = performance.now(); 36 while (performance.now() - startTime < 1) { 37 // Artificial delay 38 } 39 40 return <li>{text}</li>; 41}

With Suspense#

1import { Suspense, useState, useDeferredValue } from 'react'; 2 3function SearchApp() { 4 const [query, setQuery] = useState(''); 5 const deferredQuery = useDeferredValue(query); 6 7 const isStale = query !== deferredQuery; 8 9 return ( 10 <div> 11 <SearchInput value={query} onChange={setQuery} /> 12 13 <Suspense fallback={<Loading />}> 14 <div style={{ opacity: isStale ? 0.7 : 1 }}> 15 <SearchResults query={deferredQuery} /> 16 </div> 17 </Suspense> 18 </div> 19 ); 20} 21 22// SearchResults can suspend while fetching 23function SearchResults({ query }) { 24 const results = use(fetchResults(query)); 25 26 return ( 27 <ul> 28 {results.map((result) => ( 29 <li key={result.id}>{result.title}</li> 30 ))} 31 </ul> 32 ); 33}

Filter Performance#

1import { useState, useDeferredValue, useMemo, memo } from 'react'; 2 3function ProductList({ products }) { 4 const [filter, setFilter] = useState(''); 5 const [category, setCategory] = useState('all'); 6 7 const deferredFilter = useDeferredValue(filter); 8 const deferredCategory = useDeferredValue(category); 9 10 const isStale = 11 filter !== deferredFilter || category !== deferredCategory; 12 13 return ( 14 <div> 15 <div className="filters"> 16 <input 17 value={filter} 18 onChange={(e) => setFilter(e.target.value)} 19 placeholder="Filter products..." 20 /> 21 22 <select 23 value={category} 24 onChange={(e) => setCategory(e.target.value)} 25 > 26 <option value="all">All Categories</option> 27 <option value="electronics">Electronics</option> 28 <option value="clothing">Clothing</option> 29 </select> 30 </div> 31 32 <div className={isStale ? 'stale' : ''}> 33 <FilteredProducts 34 products={products} 35 filter={deferredFilter} 36 category={deferredCategory} 37 /> 38 </div> 39 </div> 40 ); 41} 42 43const FilteredProducts = memo(function FilteredProducts({ 44 products, 45 filter, 46 category, 47}) { 48 const filtered = useMemo(() => { 49 return products.filter((product) => { 50 const matchesFilter = product.name 51 .toLowerCase() 52 .includes(filter.toLowerCase()); 53 const matchesCategory = 54 category === 'all' || product.category === category; 55 return matchesFilter && matchesCategory; 56 }); 57 }, [products, filter, category]); 58 59 return ( 60 <div className="product-grid"> 61 {filtered.map((product) => ( 62 <ProductCard key={product.id} product={product} /> 63 ))} 64 </div> 65 ); 66});

Chart Updates#

1import { useState, useDeferredValue, memo } from 'react'; 2 3function Dashboard() { 4 const [data, setData] = useState(initialData); 5 const [range, setRange] = useState({ start: 0, end: 100 }); 6 7 const deferredRange = useDeferredValue(range); 8 const isStale = range !== deferredRange; 9 10 return ( 11 <div> 12 <RangeSlider 13 value={range} 14 onChange={setRange} 15 /> 16 17 <div style={{ opacity: isStale ? 0.6 : 1 }}> 18 <ExpensiveChart 19 data={data} 20 range={deferredRange} 21 /> 22 </div> 23 </div> 24 ); 25} 26 27const ExpensiveChart = memo(function ExpensiveChart({ data, range }) { 28 // Expensive rendering with many data points 29 const filteredData = data.slice(range.start, range.end); 30 31 return ( 32 <svg width="800" height="400"> 33 {filteredData.map((point, i) => ( 34 <circle 35 key={i} 36 cx={point.x} 37 cy={point.y} 38 r="2" 39 fill="blue" 40 /> 41 ))} 42 </svg> 43 ); 44});

Table Filtering#

1import { useState, useDeferredValue, useMemo, memo } from 'react'; 2 3function DataTable({ rows }) { 4 const [sortColumn, setSortColumn] = useState('name'); 5 const [sortDirection, setSortDirection] = useState('asc'); 6 const [search, setSearch] = useState(''); 7 8 const deferredSearch = useDeferredValue(search); 9 const isSearching = search !== deferredSearch; 10 11 return ( 12 <div> 13 <input 14 value={search} 15 onChange={(e) => setSearch(e.target.value)} 16 placeholder="Search table..." 17 /> 18 19 {isSearching && <span className="searching">Searching...</span>} 20 21 <TableBody 22 rows={rows} 23 search={deferredSearch} 24 sortColumn={sortColumn} 25 sortDirection={sortDirection} 26 /> 27 </div> 28 ); 29} 30 31const TableBody = memo(function TableBody({ 32 rows, 33 search, 34 sortColumn, 35 sortDirection, 36}) { 37 const processedRows = useMemo(() => { 38 let result = rows; 39 40 // Filter 41 if (search) { 42 result = result.filter((row) => 43 Object.values(row).some((value) => 44 String(value).toLowerCase().includes(search.toLowerCase()) 45 ) 46 ); 47 } 48 49 // Sort 50 result = [...result].sort((a, b) => { 51 const aVal = a[sortColumn]; 52 const bVal = b[sortColumn]; 53 const direction = sortDirection === 'asc' ? 1 : -1; 54 return aVal > bVal ? direction : -direction; 55 }); 56 57 return result; 58 }, [rows, search, sortColumn, sortDirection]); 59 60 return ( 61 <tbody> 62 {processedRows.map((row) => ( 63 <tr key={row.id}> 64 {Object.values(row).map((cell, i) => ( 65 <td key={i}>{cell}</td> 66 ))} 67 </tr> 68 ))} 69 </tbody> 70 ); 71});

vs useTransition#

1import { useState, useTransition, useDeferredValue } from 'react'; 2 3// useTransition - you control when to defer 4function WithTransition() { 5 const [query, setQuery] = useState(''); 6 const [isPending, startTransition] = useTransition(); 7 8 const handleChange = (e) => { 9 // Input updates immediately 10 setQuery(e.target.value); 11 12 // Results update in transition 13 startTransition(() => { 14 setQuery(e.target.value); 15 }); 16 }; 17 18 return ( 19 <div> 20 <input onChange={handleChange} /> 21 <Results query={query} /> 22 </div> 23 ); 24} 25 26// useDeferredValue - React controls deferral 27function WithDeferredValue() { 28 const [query, setQuery] = useState(''); 29 const deferredQuery = useDeferredValue(query); 30 31 // React automatically defers based on value 32 return ( 33 <div> 34 <input 35 value={query} 36 onChange={(e) => setQuery(e.target.value)} 37 /> 38 <Results query={deferredQuery} /> 39 </div> 40 ); 41} 42 43// Key difference: 44// - useTransition: wrap the state update 45// - useDeferredValue: wrap the value usage

Debouncing Alternative#

1// Traditional debouncing 2function DebounceSearch() { 3 const [query, setQuery] = useState(''); 4 const [debouncedQuery, setDebouncedQuery] = useState(''); 5 6 useEffect(() => { 7 const timer = setTimeout(() => { 8 setDebouncedQuery(query); 9 }, 300); 10 11 return () => clearTimeout(timer); 12 }, [query]); 13 14 return <Results query={debouncedQuery} />; 15} 16 17// useDeferredValue - no fixed delay 18function DeferredSearch() { 19 const [query, setQuery] = useState(''); 20 const deferredQuery = useDeferredValue(query); 21 22 // Defers based on device capability 23 // Fast devices: minimal delay 24 // Slow devices: longer delay to keep responsive 25 26 return <Results query={deferredQuery} />; 27} 28 29// Combine both for best results 30function OptimalSearch() { 31 const [query, setQuery] = useState(''); 32 const deferredQuery = useDeferredValue(query); 33 34 // Debounce network requests 35 // Defer expensive renders 36 useEffect(() => { 37 if (!deferredQuery) return; 38 39 const timer = setTimeout(() => { 40 fetchResults(deferredQuery); 41 }, 300); 42 43 return () => clearTimeout(timer); 44 }, [deferredQuery]); 45 46 return <Results query={deferredQuery} />; 47}

Best Practices#

Usage: ✓ Defer expensive renders ✓ Keep input responsive ✓ Show stale indicators ✓ Memoize child components When to Use: ✓ Filtering large lists ✓ Complex visualizations ✓ Real-time search ✓ Expensive computations Patterns: ✓ Combine with memo() ✓ Track stale state ✓ Use with Suspense ✓ Apply visual feedback Avoid: ✗ Deferring fast operations ✗ Without memoization ✗ For network requests alone ✗ Nested deferred values

Conclusion#

The useDeferredValue hook improves UI responsiveness by deferring non-urgent updates. Use it for expensive renders like filtering large lists or rendering complex charts. Always memoize child components and provide visual feedback when showing stale content. It adapts to device capabilities automatically, unlike fixed debounce delays.

Share this article

Help spread the word about Bootspring