JavaScript and React course Β· Module 9: Modern React Hooks

useDeferredValue - deferred values for performance

16 min read
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}
45

Both 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});
29

The 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}
78

The "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]);
36

The 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}
178

The 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:

Useful articles