JavaScript and React course Β· Module 9: Modern React Hooks
useDeferredValue - deferred values for performance
In this lesson3
In the useTransition lesson, you were the one deciding which state change was less urgent. But what if the value comes from props, from a library or from someone else's hook, and you have no access to its setter? The ship's radar reports a new position every fraction of a second, and the heavy galaxy map cannot keep up with drawing.
The useDeferredValue hook complements useTransition, allowing you to "defer" a value so that React can treat updates as less urgent. It is particularly useful when you want to keep the UI responsive while rendering heavy components.
useDeferredValue Basics
1. Syntax and basic usage
You pass any value to the hook, and it returns a "lagging" copy of it. On a change, React first renders the screen with the old copy, then does the new render in the background and can interrupt it. In a search box it looks like this:
1import { useDeferredValue, useState } from 'react';
2
3function SearchApp() {
4 const [query, setQuery] = useState('');
5 const deferredQuery = useDeferredValue(query);
6
7 return (
8 <div>
9 <input
10 value={query}
11 onChange={(e) => setQuery(e.target.value)}
12 />
13
14 {/* Input updates immediately */}
15 <p>Current query: {query}</p>
16
17 {/* List uses the deferred value */}
18 <SearchResults query={deferredQuery} />
19 </div>
20 );
21}
22
23function SearchResults({ query }) {
24 // Heavy computations with deferred value
25 const results = performHeavySearch(query);
26
27 return (
28 <div>
29 <p>Results for: "{query}"</p>
30 <ul>
31 {results.map(result => (
32 <li key={result.id}>{result.title}</li>
33 ))}
34 </ul>
35 </div>
36 );
37}
38
39function performHeavySearch(query) {
40 // Simulation of heavy computations
41 const items = [];
42 for (let i = 0; i < 5000; i++) {
43 if (`Item ${i}`.toLowerCase().includes(query.toLowerCase())) {
44 items.push({ id: i, title: `Item ${i}` });
45 }
46 }
47 return items;
48}The field and the "Current query" text read the fresh query, while the list gets deferredQuery. There is a catch, pointed out by react.dev: this optimization only works when the heavy component is wrapped in memo. Here SearchResults has no memo, so during the urgent render it still recomputes the results, just for the old value. We will fix that in section 3.
2. Difference between useDeferredValue and useTransition
Let's compare two ways of achieving the same effect. In the first we defer a value, in the second we split the state into urgent and transitional ourselves:
1import { useDeferredValue, useTransition, useState } from 'react';
2
3// Approach 1: useDeferredValue
4function DeferredValueExample() {
5 const [input, setInput] = useState('');
6 const deferredInput = useDeferredValue(input);
7
8 return (
9 <div>
10 <h3>useDeferredValue</h3>
11 <input
12 value={input}
13 onChange={(e) => setInput(e.target.value)}
14 />
15 <HeavyComponent text={deferredInput} />
16 </div>
17 );
18}
19
20// Approach 2: useTransition
21function TransitionExample() {
22 const [input, setInput] = useState('');
23 const [deferredInput, setDeferredInput] = useState('');
24 const [isPending, startTransition] = useTransition();
25
26 const handleChange = (value) => {
27 setInput(value); // Immediate update
28 startTransition(() => {
29 setDeferredInput(value); // Deferred update
30 });
31 };
32
33 return (
34 <div>
35 <h3>useTransition</h3>
36 <input
37 value={input}
38 onChange={(e) => handleChange(e.target.value)}
39 />
40 {isPending && <div>Updating...</div>}
41 <HeavyComponent text={deferredInput} />
42 </div>
43 );
44}
45Both components behave similarly, but useTransition needs access to the place where state is updated and gives you an isPending flag. useDeferredValue, on the other hand, only needs the value. Here is the shared heavy component and both versions side by side:
1function HeavyComponent({ text }) {
2 // Simulation of heavy rendering
3 const items = [];
4 for (let i = 0; i < 1000; i++) {
5 items.push(`${text} - element ${i}`);
6 }
7
8 return (
9 <div style={{ height: '200px', overflow: 'auto' }}>
10 {items.map((item, index) => (
11 <div key={index} style={{ padding: '2px' }}>
12 {item}
13 </div>
14 ))}
15 </div>
16 );
17}
18
19// Comparison of both approaches
20function ComparisonDemo() {
21 return (
22 <div style={{ display: 'flex', gap: '20px' }}>
23 <div style={{ flex: 1 }}>
24 <DeferredValueExample />
25 </div>
26 <div style={{ flex: 1 }}>
27 <TransitionExample />
28 </div>
29 </div>
30 );
31}My recommendation: if you have access to the setter, usually choose useTransition. If you receive a ready-made value from props or an external hook, reach for useDeferredValue.
3. Working with memo for optimization
Now a version where deferring really works. The list is wrapped in memo, so React skips its render as long as its props do not change:
1import { useDeferredValue, useState, useMemo, memo } from 'react';
2
3// Component optimized with memo
4const OptimizedList = memo(function OptimizedList({ items, highlightTerm }) {
5 console.log('OptimizedList renders for:', highlightTerm);
6
7 return (
8 <div style={{ height: '300px', overflow: 'auto', border: '1px solid #ddd' }}>
9 {items.map(item => (
10 <div
11 key={item.id}
12 style={{
13 padding: '8px',
14 borderBottom: '1px solid #eee',
15 backgroundColor: item.id % 2 === 0 ? '#f9f9f9' : '#fff'
16 }}
17 >
18 <strong>
19 {highlightText(item.title, highlightTerm)}
20 </strong>
21 <div style={{ fontSize: '14px', color: '#666' }}>
22 {highlightText(item.description, highlightTerm)}
23 </div>
24 </div>
25 ))}
26 </div>
27 );
28});
29The console.log message lets you check when the list actually renders. The second part is the search component with its data and a "staleness" flag:
1function SearchWithOptimization() {
2 const [searchTerm, setSearchTerm] = useState('');
3 const deferredSearchTerm = useDeferredValue(searchTerm);
4
5 // Generating data
6 const allItems = useMemo(() => generateSearchData(2000), []);
7
8 // Filtering with deferred value
9 const filteredItems = useMemo(() => allItems.filter(item =>
10 item.title.toLowerCase().includes(deferredSearchTerm.toLowerCase()) ||
11 item.description.toLowerCase().includes(deferredSearchTerm.toLowerCase())
12 ), [allItems, deferredSearchTerm]);
13
14 const isStale = searchTerm !== deferredSearchTerm;
15
16 return (
17 <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
18 <h2>Search with useDeferredValue</h2>
19
20 <input
21 value={searchTerm}
22 onChange={(e) => setSearchTerm(e.target.value)}
23 style={{
24 width: '100%',
25 padding: '12px',
26 fontSize: '16px',
27 border: '2px solid #ddd',
28 borderRadius: '8px',
29 marginBottom: '10px'
30 }}
31 />
32
33 <div style={{
34 display: 'flex',
35 justifyContent: 'space-between',
36 alignItems: 'center',
37 marginBottom: '10px',
38 fontSize: '14px'
39 }}>
40 <span>
41 Found {filteredItems.length} results
42 {deferredSearchTerm && ` for "${deferredSearchTerm}"`}
43 </span>
44
45 {isStale && (
46 <span style={{ color: '#007bff', fontStyle: 'italic' }}>
47 Updating...
48 </span>
49 )}
50 </div>
51
52 <div style={{ opacity: isStale ? 0.7 : 1, transition: 'opacity 0.2s' }}>
53 <OptimizedList
54 items={filteredItems}
55 highlightTerm={deferredSearchTerm}
56 />
57 </div>
58 </div>
59 );
60}
61
62function generateSearchData(count) {
63 const categories = ['Electronics', 'Books', 'Clothing', 'Home', 'Sports'];
64 const adjectives = ['Modern', 'Classic', 'Premium', 'Budget', 'Universal'];
65
66 return Array.from({ length: count }, (_, i) => ({
67 id: i,
68 title: `${adjectives[i % adjectives.length]} Product ${i + 1}`,
69 description: `Product description from the ${categories[i % categories.length]} category. High-quality item.`,
70 category: categories[i % categories.length],
71 price: Math.floor(Math.random() * 1000) + 50
72 }));
73}
74
75function highlightText(text, term) {
76 if (!term) return text;
77
78 const regex = new RegExp(`(${term})`, 'gi');
79 const parts = text.split(regex);
80
81 return parts.map((part, index) =>
82 part.toLowerCase() === term.toLowerCase() ? (
83 <mark key={index} style={{ backgroundColor: '#ffeb3b', padding: '2px' }}>
84 {part}
85 </mark>
86 ) : part
87 );
88}The comparison searchTerm !== deferredSearchTerm tells us the screen still shows old results, so we dim the list. We keep the data and the filtering in useMemo. Without it, every render would create a new filteredItems array, memo would always see new props and skip nothing. Since React 19 the hook also takes a second argument, useDeferredValue(value, initialValue), the value used during the initial render.
Advanced patterns with useDeferredValue
1. Adaptive loading with fallback
A deferred value also helps when switching tabs. A click highlights the new tab immediately, and the content catches up a moment later:
1import { useDeferredValue, useState, Suspense } from 'react';
2
3function AdaptiveLoadingDemo() {
4 const [selectedTab, setSelectedTab] = useState('popular');
5 const deferredTab = useDeferredValue(selectedTab);
6
7 const tabs = [
8 { id: 'popular', label: 'Popular' },
9 { id: 'newest', label: 'Newest' },
10 { id: 'trending', label: 'Trending' }
11 ];
12
13 return (
14 <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
15 <h2>Adaptive Loading with useDeferredValue</h2>
16
17 {/* Tab Navigation */}
18 <div style={{
19 display: 'flex',
20 marginBottom: '20px',
21 borderBottom: '1px solid #ddd'
22 }}>
23 {tabs.map(tab => (
24 <button
25 key={tab.id}
26 onClick={() => setSelectedTab(tab.id)}
27 style={{
28 padding: '12px 20px',
29 border: 'none',
30 backgroundColor: selectedTab === tab.id ? '#007bff' : 'transparent',
31 color: selectedTab === tab.id ? 'white' : '#007bff',
32 cursor: 'pointer',
33 borderRadius: '8px 8px 0 0',
34 marginRight: '5px',
35 fontSize: '16px'
36 }}
37 >
38 {tab.label}
39 </button>
40 ))}
41 </div>
42
43 {/* Content with deferred loading */}
44 <div style={{ position: 'relative' }}>
45 {/* Show loading state when tab is changing */}
46 {selectedTab !== deferredTab && (
47 <div style={{
48 position: 'absolute',
49 top: 0,
50 left: 0,
51 right: 0,
52 bottom: 0,
53 backgroundColor: 'rgba(255, 255, 255, 0.8)',
54 display: 'flex',
55 alignItems: 'center',
56 justifyContent: 'center',
57 zIndex: 1,
58 borderRadius: '8px'
59 }}>
60 <div style={{
61 padding: '20px',
62 backgroundColor: 'white',
63 borderRadius: '8px',
64 boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
65 }}>
66 Loading content...
67 </div>
68 </div>
69 )}
70
71 <Suspense fallback={<TabContentSkeleton />}>
72 <TabContent tab={deferredTab} />
73 </Suspense>
74 </div>
75 </div>
76 );
77}
78The "Loading content..." overlay appears when selectedTab and deferredTab differ. Below are the content component, the skeleton and the data:
1function TabContent({ tab }) {
2 // Simulation of heavy data loading
3 const content = generateTabContent(tab);
4
5 return (
6 <div style={{
7 minHeight: '400px',
8 padding: '20px',
9 backgroundColor: '#f8f9fa',
10 borderRadius: '8px'
11 }}>
12 <h3>Tab content: {tab}</h3>
13 <div style={{ display: 'grid', gap: '15px' }}>
14 {content.map(item => (
15 <div
16 key={item.id}
17 style={{
18 padding: '15px',
19 backgroundColor: 'white',
20 borderRadius: '6px',
21 boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
22 }}
23 >
24 <h4 style={{ margin: '0 0 8px 0', color: '#333' }}>
25 {item.title}
26 </h4>
27 <p style={{ margin: 0, color: '#666', fontSize: '14px' }}>
28 {item.description}
29 </p>
30 <div style={{
31 marginTop: '8px',
32 fontSize: '12px',
33 color: '#999'
34 }}>
35 {item.date} β’ {item.views} views
36 </div>
37 </div>
38 ))}
39 </div>
40 </div>
41 );
42}
43
44function TabContentSkeleton() {
45 return (
46 <div style={{
47 minHeight: '400px',
48 padding: '20px',
49 backgroundColor: '#f8f9fa',
50 borderRadius: '8px'
51 }}>
52 <div style={{
53 height: '24px',
54 backgroundColor: '#e9ecef',
55 borderRadius: '4px',
56 marginBottom: '20px',
57 width: '200px'
58 }} />
59
60 {Array.from({ length: 5 }).map((_, i) => (
61 <div
62 key={i}
63 style={{
64 padding: '15px',
65 backgroundColor: 'white',
66 borderRadius: '6px',
67 marginBottom: '15px',
68 boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
69 }}
70 >
71 <div style={{
72 height: '18px',
73 backgroundColor: '#e9ecef',
74 borderRadius: '4px',
75 marginBottom: '8px',
76 width: '70%'
77 }} />
78 <div style={{
79 height: '14px',
80 backgroundColor: '#f1f3f4',
81 borderRadius: '4px',
82 marginBottom: '4px'
83 }} />
84 <div style={{
85 height: '14px',
86 backgroundColor: '#f1f3f4',
87 borderRadius: '4px',
88 width: '60%'
89 }} />
90 </div>
91 ))}
92 </div>
93 );
94}
95
96function generateTabContent(tab) {
97 const contentMap = {
98 popular: [
99 { id: 1, title: 'Most popular article', description: 'This article was the most popular this week.', date: '2024-01-15', views: 15420 },
100 { id: 2, title: 'Second in ranking', description: 'Great material that earned reader recognition.', date: '2024-01-14', views: 12380 },
101 { id: 3, title: 'Top 3 popularity', description: 'Also a very interesting article with a high rating.', date: '2024-01-13', views: 9560 }
102 ],
103 newest: [
104 { id: 4, title: 'Latest post', description: 'Freshly published blog material.', date: '2024-01-16', views: 245 },
105 { id: 5, title: 'Yesterday\'s article', description: 'Published yesterday, already gaining popularity.', date: '2024-01-15', views: 1230 },
106 { id: 6, title: 'From 2 days ago', description: 'Still relevant and valuable material.', date: '2024-01-14', views: 2100 }
107 ],
108 trending: [
109 { id: 7, title: 'Viral on social media', description: 'This article is taking social media by storm!', date: '2024-01-12', views: 25600 },
110 { id: 8, title: 'Rising popularity', description: 'Quickly gaining popularity.', date: '2024-01-11', views: 8970 },
111 { id: 9, title: 'Trend of the week', description: 'The main trend of this week in our industry.', date: '2024-01-10', views: 14520 }
112 ]
113 };
114
115 return contentMap[tab] || [];
116}Note that TabContent is synchronous here, so you will only see the TabContentSkeleton fallback when the component really suspends, for example while reading data through Suspense. With data loaded that way, useDeferredValue keeps showing the old content instead of the fallback.
2. Several filters at once
Several filters at once are a good candidate for several deferred values. First the state, the deferred copies and the memoized filtering:
1import { useDeferredValue, useState, useMemo } from 'react';
2
3function SmartFilteringDemo() {
4 const [searchQuery, setSearchQuery] = useState('');
5 const [priceRange, setPriceRange] = useState([0, 1000]);
6 const [category, setCategory] = useState('all');
7
8 // Deferred values for heavy operations
9 const deferredQuery = useDeferredValue(searchQuery);
10 const deferredPriceRange = useDeferredValue(priceRange);
11 const deferredCategory = useDeferredValue(category);
12
13 // Check if filtering is in progress
14 const isFiltering =
15 searchQuery !== deferredQuery ||
16 priceRange !== deferredPriceRange ||
17 category !== deferredCategory;
18
19 const products = useMemo(() => generateProducts(1000), []);
20
21 // Filtering with deferred values
22 const filteredProducts = useMemo(() => {
23 return products.filter(product => {
24 const matchesSearch = product.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
25 product.description.toLowerCase().includes(deferredQuery.toLowerCase());
26
27 const matchesPrice = product.price >= deferredPriceRange[0] &&
28 product.price <= deferredPriceRange[1];
29
30 const matchesCategory = deferredCategory === 'all' ||
31 product.category === deferredCategory;
32
33 return matchesSearch && matchesPrice && matchesCategory;
34 });
35 }, [products, deferredQuery, deferredPriceRange, deferredCategory]);
36The isFiltering flag compares every pair of values. The priceRange array is compared by reference, which works because every slider change creates a new array. Now the filter panel and the results grid:
1 return (
2 <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
3 <h2>Smart Filtering with useDeferredValue</h2>
4
5 <div style={{
6 display: 'grid',
7 gridTemplateColumns: '300px 1fr',
8 gap: '20px'
9 }}>
10 {/* Filters Sidebar */}
11 <div style={{
12 padding: '20px',
13 backgroundColor: '#f8f9fa',
14 borderRadius: '8px',
15 height: 'fit-content'
16 }}>
17 <h3 style={{ marginTop: 0 }}>Filters</h3>
18
19 {/* Search Filter */}
20 <div style={{ marginBottom: '20px' }}>
21 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
22 Search:
23 </label>
24 <input
25 value={searchQuery}
26 onChange={(e) => setSearchQuery(e.target.value)}
27 style={{
28 width: '100%',
29 padding: '8px',
30 border: '1px solid #ddd',
31 borderRadius: '4px'
32 }}
33 />
34 </div>
35
36 {/* Category Filter */}
37 <div style={{ marginBottom: '20px' }}>
38 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
39 Category:
40 </label>
41 <select
42 value={category}
43 onChange={(e) => setCategory(e.target.value)}
44 style={{
45 width: '100%',
46 padding: '8px',
47 border: '1px solid #ddd',
48 borderRadius: '4px'
49 }}
50 >
51 <option value="all">All</option>
52 <option value="Electronics">Electronics</option>
53 <option value="Books">Books</option>
54 <option value="Clothing">Clothing</option>
55 <option value="Home">Home</option>
56 </select>
57 </div>
58
59 {/* Price Range Filter */}
60 <div style={{ marginBottom: '20px' }}>
61 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
62 Price: {priceRange[0]} - {priceRange[1]} $
63 </label>
64 <input
65 type="range"
66 min="0"
67 max="1000"
68 value={priceRange[1]}
69 onChange={(e) => setPriceRange([priceRange[0], parseInt(e.target.value)])}
70 style={{ width: '100%' }}
71 />
72 </div>
73
74 {/* Filter Status */}
75 <div style={{
76 padding: '10px',
77 backgroundColor: isFiltering ? '#fff3cd' : '#d4edda',
78 borderRadius: '4px',
79 fontSize: '14px',
80 textAlign: 'center'
81 }}>
82 {isFiltering ? (
83 <span>Filtering...</span>
84 ) : (
85 <span>Done</span>
86 )}
87 </div>
88 </div>
89
90 {/* Results */}
91 <div>
92 <div style={{
93 display: 'flex',
94 justifyContent: 'space-between',
95 alignItems: 'center',
96 marginBottom: '20px'
97 }}>
98 <h3 style={{ margin: 0 }}>
99 Products ({filteredProducts.length})
100 </h3>
101
102 {isFiltering && (
103 <span style={{ color: '#007bff', fontSize: '14px' }}>
104 Updating results...
105 </span>
106 )}
107 </div>
108
109 <div style={{
110 opacity: isFiltering ? 0.7 : 1,
111 transition: 'opacity 0.3s',
112 display: 'grid',
113 gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
114 gap: '15px'
115 }}>
116 {filteredProducts.slice(0, 20).map(product => (
117 <div
118 key={product.id}
119 style={{
120 padding: '15px',
121 backgroundColor: 'white',
122 border: '1px solid #e9ecef',
123 borderRadius: '8px',
124 boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
125 }}
126 >
127 <h4 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>
128 {highlightText(product.name, deferredQuery)}
129 </h4>
130 <p style={{
131 margin: '0 0 8px 0',
132 fontSize: '14px',
133 color: '#666',
134 lineHeight: '1.4'
135 }}>
136 {highlightText(product.description, deferredQuery)}
137 </p>
138 <div style={{
139 display: 'flex',
140 justifyContent: 'space-between',
141 alignItems: 'center',
142 fontSize: '14px'
143 }}>
144 <span style={{
145 backgroundColor: '#e9ecef',
146 padding: '2px 6px',
147 borderRadius: '4px',
148 fontSize: '12px'
149 }}>
150 {product.category}
151 </span>
152 <span style={{
153 fontWeight: 'bold',
154 color: '#007bff',
155 fontSize: '16px'
156 }}>
157 ${product.price}
158 </span>
159 </div>
160 </div>
161 ))}
162 </div>
163
164 {filteredProducts.length === 0 && (
165 <div style={{
166 textAlign: 'center',
167 padding: '40px',
168 color: '#666'
169 }}>
170 No products match the criteria
171 </div>
172 )}
173 </div>
174 </div>
175 </div>
176 );
177}
178The controls read fresh values, while the product grid uses the deferred ones. Finally, the data generator and the highlighting:
1function generateProducts(count) {
2 const categories = ['Electronics', 'Books', 'Clothing', 'Home'];
3 const adjectives = ['Premium', 'Classic', 'Modern', 'Vintage', 'Professional'];
4 const nouns = ['Device', 'Tool', 'Item', 'Product', 'Gadget'];
5
6 return Array.from({ length: count }, (_, i) => ({
7 id: i,
8 name: `${adjectives[i % adjectives.length]} ${nouns[i % nouns.length]} ${i + 1}`,
9 description: `High-quality ${categories[i % categories.length].toLowerCase()} item with excellent features and durability.`,
10 category: categories[i % categories.length],
11 price: Math.floor(Math.random() * 950) + 50
12 }));
13}
14
15function highlightText(text, term) {
16 if (!term) return text;
17
18 const regex = new RegExp(`(${term})`, 'gi');
19 const parts = text.split(regex);
20
21 return parts.map((part, index) =>
22 part.toLowerCase() === term.toLowerCase() ? (
23 <mark key={index} style={{ backgroundColor: '#ffeb3b', padding: '1px' }}>
24 {part}
25 </mark>
26 ) : part
27 );
28}Unlike debouncing, there is no fixed delay here: React defers the render only as long as the device needs.
Best practices with useDeferredValue
1. When to use useDeferredValue
The examples below show where deferring pays off and where it only adds work. The key is the render cost of the component that receives the deferred value:
1// GOOD - Heavy lists and filtering
2function ProductSearch() {
3 const [query, setQuery] = useState('');
4 const deferredQuery = useDeferredValue(query);
5
6 return (
7 <>
8 <input value={query} onChange={e => setQuery(e.target.value)} />
9 <HeavyProductList searchTerm={deferredQuery} />
10 </>
11 );
12}
13
14// GOOD - Rendering charts
15function ChartDashboard() {
16 const [dateRange, setDateRange] = useState([startDate, endDate]);
17 const deferredDateRange = useDeferredValue(dateRange);
18
19 return (
20 <>
21 <DateRangePicker onChange={setDateRange} />
22 <ExpensiveChart dateRange={deferredDateRange} />
23 </>
24 );
25}
26
27// BAD - Simple operations
28function SimpleCounter() {
29 const [count, setCount] = useState(0);
30 const deferredCount = useDeferredValue(count); // Unnecessary!
31
32 return <div>{deferredCount}</div>;
33}The counter in SimpleCounter renders a single number, so deferring speeds nothing up and only adds a second render.
2. Combining with other hooks
In the data table we combine useDeferredValue with useMemo and useCallback. Filtering and sorting depend only on the deferred values:
1import { useDeferredValue, useState, useMemo, useCallback } from 'react';
2
3function OptimizedDataTable() {
4 const [searchTerm, setSearchTerm] = useState('');
5 const [sortConfig, setSortConfig] = useState({ field: 'name', direction: 'asc' });
6
7 const deferredSearchTerm = useDeferredValue(searchTerm);
8 const deferredSortConfig = useDeferredValue(sortConfig);
9
10 const data = useMemo(() => generateLargeDataset(5000), []);
11
12 // Memoized filtering and sorting
13 const processedData = useMemo(() => {
14 let filtered = data;
15
16 if (deferredSearchTerm) {
17 filtered = data.filter(item =>
18 item.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
19 );
20 }
21
22 return [...filtered].sort((a, b) => {
23 const direction = deferredSortConfig.direction === 'asc' ? 1 : -1;
24 return String(a[deferredSortConfig.field])
25 .localeCompare(String(b[deferredSortConfig.field]), undefined, { numeric: true }) * direction;
26 });
27 }, [data, deferredSearchTerm, deferredSortConfig]);
28
29 const handleSort = useCallback((field) => {
30 setSortConfig(prev => ({
31 field,
32 direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc'
33 }));
34 }, []);
35
36 const isStale = searchTerm !== deferredSearchTerm || sortConfig !== deferredSortConfig;
37
38 return (
39 <div>
40 <input
41 value={searchTerm}
42 onChange={(e) => setSearchTerm(e.target.value)}
43 />
44
45 <table style={{ opacity: isStale ? 0.7 : 1 }}>
46 <thead>
47 <tr>
48 <th onClick={() => handleSort('name')}>Name</th>
49 <th onClick={() => handleSort('category')}>Category</th>
50 <th onClick={() => handleSort('price')}>Price</th>
51 </tr>
52 </thead>
53 <tbody>
54 {processedData.slice(0, 50).map(item => (
55 <tr key={item.id}>
56 <td>{item.name}</td>
57 <td>{item.category}</td>
58 <td>{item.price}</td>
59 </tr>
60 ))}
61 </tbody>
62 </table>
63 </div>
64 );
65}We sort a copy, [...filtered], because sort mutates the array in place and without a copy it would corrupt the memoized data. Comparing through String with the numeric option also handles the price column, which holds numbers.
3. What to avoid
The last example shows too much of a good thing. Three separate deferred values for a simple form only complicate the code:
1// BAD - Overuse
2function OverusedDeferred() {
3 const [name, setName] = useState('');
4 const [age, setAge] = useState(0);
5 const [email, setEmail] = useState('');
6
7 // Not every value needs deferred!
8 const deferredName = useDeferredValue(name);
9 const deferredAge = useDeferredValue(age);
10 const deferredEmail = useDeferredValue(email);
11
12 return <SimpleForm name={deferredName} age={deferredAge} email={deferredEmail} />;
13}
14
15// BETTER - Selective use
16function BetterApproach() {
17 const [formData, setFormData] = useState({ name: '', age: 0, email: '' });
18 const deferredFormData = useDeferredValue(formData);
19
20 return <HeavyValidationForm data={deferredFormData} />;
21}It is better to defer a single object passed to a truly heavy component.
useDeferredValue is an elegant way to improve the performance of React applications without complex state management. It allows you to keep critical UI elements responsive while performing heavy operations in the background.
Measure before you optimize: you will learn list virtualization in the next lesson. Remember: a deferred value is a radar echo that the galaxy map reads as soon as the pilot finishes the maneuver.
Code for this lesson: App.jsx
1import { useDeferredValue, useState, memo, useMemo } from 'react';
2
3// Simulation of heavy operation - rendering many elements
4function generateItems(count, filter = '') {
5 const items = [];
6 for (let i = 0; i < count; i++) {
7 const name = `Product ${i + 1}`;
8 const category = ['Electronics', 'Books', 'Clothing', 'Home', 'Sports'][i % 5];
9
10 if (filter === '' ||
11 name.toLowerCase().includes(filter.toLowerCase()) ||
12 category.toLowerCase().includes(filter.toLowerCase())) {
13 items.push({
14 id: i,
15 name,
16 category,
17 price: Math.floor(Math.random() * 1000) + 10,
18 rating: (Math.random() * 5).toFixed(1),
19 inStock: Math.random() > 0.2
20 });
21 }
22 }
23 return items;
24}
25
26// Component optimized with memo
27const ProductList = memo(function ProductList({ items, isStale }) {
28 // Simulation of heavy computations
29 const processedItems = useMemo(() => {
30 return items.map(item => ({
31 ...item,
32 displayPrice: `${item.price} credits`,
33 ratingStars: 'β'.repeat(Math.floor(item.rating)) + 'β'.repeat(5 - Math.floor(item.rating))
34 }));
35 }, [items]);
36
37 return (
38 <div style={{
39 opacity: isStale ? 0.6 : 1,
40 transition: 'opacity 0.2s',
41 height: '500px',
42 overflow: 'auto',
43 border: '1px solid #ddd',
44 borderRadius: '8px',
45 backgroundColor: '#fff'
46 }}>
47 {isStale && (
48 <div style={{
49 position: 'sticky',
50 top: 0,
51 backgroundColor: '#fff3cd',
52 padding: '8px 16px',
53 borderBottom: '1px solid #ffeaa7',
54 fontSize: '14px',
55 color: '#856404'
56 }}>
57 Updating results...
58 </div>
59 )}
60
61 {processedItems.length === 0 ? (
62 <div style={{
63 padding: '40px',
64 textAlign: 'center',
65 color: '#666'
66 }}>
67 No products matching criteria
68 </div>
69 ) : (
70 <div style={{ padding: '16px' }}>
71 <div style={{
72 marginBottom: '16px',
73 fontSize: '14px',
74 color: '#666',
75 borderBottom: '1px solid #eee',
76 paddingBottom: '8px'
77 }}>
78 Found {processedItems.length} products
79 </div>
80
81 <div style={{
82 display: 'grid',
83 gap: '16px',
84 gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
85 }}>
86 {processedItems.map(item => (
87 <div
88 key={item.id}
89 style={{
90 padding: '16px',
91 border: '1px solid #e9ecef',
92 borderRadius: '8px',
93 backgroundColor: '#f8f9fa'
94 }}
95 >
96 <div style={{
97 display: 'flex',
98 justifyContent: 'space-between',
99 alignItems: 'flex-start',
100 marginBottom: '8px'
101 }}>
102 <h3 style={{
103 margin: 0,
104 fontSize: '16px',
105 color: '#333'
106 }}>
107 {item.name}
108 </h3>
109 <span style={{
110 fontSize: '12px',
111 padding: '4px 8px',
112 backgroundColor: '#007bff',
113 color: 'white',
114 borderRadius: '12px'
115 }}>
116 {item.category}
117 </span>
118 </div>
119
120 <div style={{
121 display: 'flex',
122 justifyContent: 'space-between',
123 alignItems: 'center',
124 marginBottom: '8px'
125 }}>
126 <span style={{
127 fontSize: '18px',
128 fontWeight: 'bold',
129 color: '#28a745'
130 }}>
131 {item.displayPrice}
132 </span>
133
134 <div style={{ fontSize: '14px', color: '#ffc107' }}>
135 {item.ratingStars} ({item.rating})
136 </div>
137 </div>
138
139 <div style={{
140 fontSize: '12px',
141 color: item.inStock ? '#28a745' : '#dc3545',
142 fontWeight: 'bold'
143 }}>
144 {item.inStock ? 'In stock' : 'Out of stock'}
145 </div>
146 </div>
147 ))}
148 </div>
149 </div>
150 )}
151 </div>
152 );
153});
154
155function DeferredValueDemo() {
156 const [query, setQuery] = useState('');
157 const [category, setCategory] = useState('');
158 const [sortBy, setSortBy] = useState('name');
159
160 // DEFERRED VALUES - will update with a delay
161 const deferredQuery = useDeferredValue(query);
162 const deferredCategory = useDeferredValue(category);
163 const deferredSortBy = useDeferredValue(sortBy);
164
165 // Check if values are stale (outdated)
166 const isStale = deferredQuery !== query ||
167 deferredCategory !== category ||
168 deferredSortBy !== sortBy;
169
170 // Generating results based on DEFERRED values
171 const results = useMemo(() => {
172 let items = generateItems(8000, deferredQuery);
173
174 // Filter by category
175 if (deferredCategory) {
176 items = items.filter(item =>
177 item.category.toLowerCase().includes(deferredCategory.toLowerCase())
178 );
179 }
180
181 // Sorting
182 items.sort((a, b) => {
183 switch (deferredSortBy) {
184 case 'price':
185 return a.price - b.price;
186 case 'rating':
187 return b.rating - a.rating;
188 case 'name':
189 default:
190 return a.name.localeCompare(b.name);
191 }
192 });
193
194 return items;
195 }, [deferredQuery, deferredCategory, deferredSortBy]);
196
197 return (
198 <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
199 <h2>useDeferredValue Demo - Online Shop</h2>
200
201 <div style={{
202 display: 'flex',
203 gap: '16px',
204 marginBottom: '20px',
205 flexWrap: 'wrap'
206 }}>
207 <div style={{ flex: '1', minWidth: '200px' }}>
208 <label style={{
209 display: 'block',
210 marginBottom: '4px',
211 fontSize: '14px',
212 fontWeight: 'bold'
213 }}>
214 Search product:
215 </label>
216 <input
217 value={query}
218 onChange={(e) => setQuery(e.target.value)}
219 placeholder="Type product name..."
220 style={{
221 width: '100%',
222 padding: '10px',
223 fontSize: '16px',
224 border: '2px solid #ddd',
225 borderRadius: '6px',
226 outline: 'none',
227 boxSizing: 'border-box'
228 }}
229 />
230 </div>
231
232 <div style={{ flex: '1', minWidth: '150px' }}>
233 <label style={{
234 display: 'block',
235 marginBottom: '4px',
236 fontSize: '14px',
237 fontWeight: 'bold'
238 }}>
239 Category:
240 </label>
241 <select
242 value={category}
243 onChange={(e) => setCategory(e.target.value)}
244 style={{
245 width: '100%',
246 padding: '10px',
247 fontSize: '16px',
248 border: '2px solid #ddd',
249 borderRadius: '6px',
250 outline: 'none',
251 backgroundColor: 'white'
252 }}
253 >
254 <option value="">All categories</option>
255 <option value="Electronics">Electronics</option>
256 <option value="Books">Books</option>
257 <option value="Clothing">Clothing</option>
258 <option value="Home">Home</option>
259 <option value="Sports">Sports</option>
260 </select>
261 </div>
262
263 <div style={{ flex: '1', minWidth: '150px' }}>
264 <label style={{
265 display: 'block',
266 marginBottom: '4px',
267 fontSize: '14px',
268 fontWeight: 'bold'
269 }}>
270 Sort by:
271 </label>
272 <select
273 value={sortBy}
274 onChange={(e) => setSortBy(e.target.value)}
275 style={{
276 width: '100%',
277 padding: '10px',
278 fontSize: '16px',
279 border: '2px solid #ddd',
280 borderRadius: '6px',
281 outline: 'none',
282 backgroundColor: 'white'
283 }}
284 >
285 <option value="name">Name</option>
286 <option value="price">Price</option>
287 <option value="rating">Rating</option>
288 </select>
289 </div>
290 </div>
291
292 <div style={{
293 display: 'flex',
294 justifyContent: 'space-between',
295 alignItems: 'center',
296 marginBottom: '16px',
297 padding: '12px',
298 backgroundColor: '#e3f2fd',
299 borderRadius: '8px',
300 fontSize: '14px'
301 }}>
302 <div>
303 <strong>Filter status:</strong>
304 <ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
305 <li>Search: "{query}" {deferredQuery !== query && '(updating...)'}</li>
306 <li>Category: {category || 'All'} {deferredCategory !== category && '(updating...)'}</li>
307 <li>Sort: {sortBy} {deferredSortBy !== sortBy && '(updating...)'}</li>
308 </ul>
309 </div>
310
311 <div style={{ textAlign: 'right' }}>
312 {isStale ? (
313 <span style={{ color: '#ff9800', fontWeight: 'bold' }}>
314 Updating results...
315 </span>
316 ) : (
317 <span style={{ color: '#4caf50', fontWeight: 'bold' }}>
318 Results up to date
319 </span>
320 )}
321 </div>
322 </div>
323
324 <ProductList items={results} isStale={isStale} />
325
326 <div style={{
327 marginTop: '20px',
328 padding: '16px',
329 backgroundColor: '#f1f8e9',
330 borderRadius: '8px',
331 fontSize: '14px'
332 }}>
333 <strong>What to observe in this demo:</strong>
334 <ul style={{ margin: '10px 0', paddingLeft: '20px' }}>
335 <li><strong>Responsive inputs:</strong> All controls react immediately</li>
336 <li><strong>Deferred rendering:</strong> Product list updates with a delay</li>
337 <li><strong>Stale indicator:</strong> Visible when data is outdated</li>
338 <li><strong>Transparency:</strong> List becomes transparent during updates</li>
339 <li><strong>Memo optimization:</strong> Component does not re-render when props are identical</li>
340 </ul>
341
342 <div style={{
343 marginTop: '12px',
344 padding: '8px',
345 backgroundColor: '#fff3e0',
346 borderRadius: '4px'
347 }}>
348 <strong>Try this:</strong> Quickly change filters and see how React prioritizes
349 input responsiveness over product list rendering!
350 </div>
351 </div>
352 </div>
353 );
354}
355
356function ComparisonDemo() {
357 const [query, setQuery] = useState('');
358 const deferredQuery = useDeferredValue(query);
359
360 // Simulation of different handling approaches
361 const [version, setVersion] = useState('deferred');
362
363 const results = useMemo(() => {
364 const searchTerm = version === 'deferred' ? deferredQuery : query;
365 return generateItems(5000, searchTerm);
366 }, [version === 'deferred' ? deferredQuery : query, version]);
367
368 return (
369 <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
370 <h2>Comparison: useDeferredValue vs no optimization</h2>
371
372 <div style={{
373 display: 'flex',
374 gap: '10px',
375 marginBottom: '20px',
376 padding: '16px',
377 backgroundColor: '#f8f9fa',
378 borderRadius: '8px'
379 }}>
380 <button
381 onClick={() => setVersion('deferred')}
382 style={{
383 padding: '10px 20px',
384 backgroundColor: version === 'deferred' ? '#28a745' : '#6c757d',
385 color: 'white',
386 border: 'none',
387 borderRadius: '4px',
388 cursor: 'pointer'
389 }}
390 >
391 With useDeferredValue
392 </button>
393 <button
394 onClick={() => setVersion('immediate')}
395 style={{
396 padding: '10px 20px',
397 backgroundColor: version === 'immediate' ? '#dc3545' : '#6c757d',
398 color: 'white',
399 border: 'none',
400 borderRadius: '4px',
401 cursor: 'pointer'
402 }}
403 >
404 Without optimization
405 </button>
406 </div>
407
408 <div style={{ marginBottom: '20px' }}>
409 <label style={{
410 display: 'block',
411 marginBottom: '8px',
412 fontSize: '16px',
413 fontWeight: 'bold'
414 }}>
415 Search products:
416 </label>
417 <input
418 value={query}
419 onChange={(e) => setQuery(e.target.value)}
420 placeholder="Try typing fast and see the difference..."
421 style={{
422 width: '100%',
423 padding: '12px',
424 fontSize: '16px',
425 border: '2px solid #ddd',
426 borderRadius: '8px',
427 outline: 'none',
428 boxSizing: 'border-box'
429 }}
430 />
431
432 <div style={{
433 marginTop: '8px',
434 fontSize: '14px',
435 color: '#666',
436 display: 'flex',
437 justifyContent: 'space-between'
438 }}>
439 <span>Current value: "{query}"</span>
440 <span>
441 {version === 'deferred' && deferredQuery !== query && (
442 <span style={{ color: '#ff9800' }}>
443 Rendering: "{deferredQuery}"
444 </span>
445 )}
446 </span>
447 </div>
448 </div>
449
450 <div style={{
451 height: '400px',
452 overflow: 'auto',
453 border: '1px solid #ddd',
454 borderRadius: '8px',
455 backgroundColor: '#fff'
456 }}>
457 {results.length === 0 ? (
458 <div style={{
459 padding: '40px',
460 textAlign: 'center',
461 color: '#666'
462 }}>
463 No results
464 </div>
465 ) : (
466 results.slice(0, 100).map(item => (
467 <div
468 key={item.id}
469 style={{
470 padding: '12px',
471 borderBottom: '1px solid #eee',
472 display: 'flex',
473 justifyContent: 'space-between',
474 alignItems: 'center'
475 }}
476 >
477 <div>
478 <div style={{ fontWeight: 'bold' }}>{item.name}</div>
479 <div style={{ fontSize: '12px', color: '#666' }}>
480 {item.category}
481 </div>
482 </div>
483 <div style={{ fontWeight: 'bold', color: '#28a745' }}>
484 {item.price} credits
485 </div>
486 </div>
487 ))
488 )}
489 </div>
490
491 <div style={{
492 marginTop: '16px',
493 padding: '12px',
494 backgroundColor: version === 'deferred' ? '#d4edda' : '#f8d7da',
495 borderRadius: '8px',
496 fontSize: '14px'
497 }}>
498 <strong>
499 {version === 'deferred' ? 'With useDeferredValue:' : 'Without optimization:'}
500 </strong>
501 <p style={{ margin: '8px 0' }}>
502 {version === 'deferred'
503 ? 'Input stays responsive, list updates with a delay when typing fast.'
504 : 'Every change in input immediately renders the entire list, which can cause lagging.'
505 }
506 </p>
507 </div>
508 </div>
509 );
510}
511
512export default function App() {
513 const [activeDemo, setActiveDemo] = useState('shop');
514
515 return (
516 <div style={{ minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
517 <nav style={{
518 backgroundColor: '#fff',
519 padding: '10px 0',
520 borderBottom: '1px solid #ddd',
521 marginBottom: '20px'
522 }}>
523 <div style={{
524 maxWidth: '1200px',
525 margin: '0 auto',
526 display: 'flex',
527 gap: '10px',
528 padding: '0 20px'
529 }}>
530 <button
531 onClick={() => setActiveDemo('shop')}
532 style={{
533 padding: '10px 20px',
534 backgroundColor: activeDemo === 'shop' ? '#007bff' : '#f8f9fa',
535 color: activeDemo === 'shop' ? 'white' : '#333',
536 border: '1px solid #ddd',
537 borderRadius: '4px',
538 cursor: 'pointer'
539 }}
540 >
541 Online Shop
542 </button>
543 <button
544 onClick={() => setActiveDemo('comparison')}
545 style={{
546 padding: '10px 20px',
547 backgroundColor: activeDemo === 'comparison' ? '#007bff' : '#f8f9fa',
548 color: activeDemo === 'comparison' ? 'white' : '#333',
549 border: '1px solid #ddd',
550 borderRadius: '4px',
551 cursor: 'pointer'
552 }}
553 >
554 Comparison
555 </button>
556 </div>
557 </nav>
558
559 {activeDemo === 'shop' ? <DeferredValueDemo /> : <ComparisonDemo />}
560 </div>
561 );
562}Spotted a mistake in this lesson?
Hands-on tasks in the game
- Horizontal ordering
Arrange the syntax of the useDeferredValue hook:
- Horizontal ordering
Arrange the destructuring syntax of the useTransition hook: