Steve Kinney

useDeferredValue Patterns

You’ve built a search component that filters a massive list of products. Your users start typing “iPhone” and immediately notice the input field feels sluggish—each keystroke seems to hang for a split second. The UI thread is getting hammered by expensive operations triggered on every character change, making your otherwise snappy interface feel clunky.

Enter useDeferredValue—React’s elegant solution for keeping high-priority updates (like user input) responsive while allowing lower-priority updates (like expensive computations) to happen when the browser has spare cycles. Think of it as your UI’s way of saying “handle the urgent stuff first, deal with the heavy lifting when we have time.”

Unlike debouncing, which delays updates by a fixed amount of time, useDeferredValue is adaptive—it defers updates only when React is busy with more important work. When your app is idle, deferred values update immediately. When React is swamped, they gracefully step aside.

The Basic Pattern

Let’s start with a simple example that demonstrates the core concept:

import { useDeferredValue, useState, useMemo } from 'react';

function SearchResults({ query }: { query: string }) {
  // Defer the expensive computation
  const deferredQuery = useDeferredValue(query);

  // Only recompute when the deferred value changes
  const results = useMemo(() => {
    return searchExpensiveDatabase(deferredQuery);
  }, [deferredQuery]);

  return (
    <div>
      {results.map((result) => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

function App() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      <SearchResults query={query} />
    </div>
  );
}

Here’s what happens: When you type rapidly, query updates immediately (keeping the input responsive), but deferredQuery only updates when React determines it’s safe to do so without blocking more urgent updates. The expensive search computation gets the deferred value, so it doesn’t interfere with typing.

Always pair

useDeferredValue with useMemo or useCallback—otherwise, your components will still re-render on every change, defeating the purpose.

Real-World Search Interface

Let’s build a more complete search interface that demonstrates several patterns working together:

import { useDeferredValue, useState, useMemo, useTransition, Suspense } from 'react';

interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  description: string;
}

function ProductSearch() {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [isPending, startTransition] = useTransition();

  // Defer the expensive filtering operation
  const deferredQuery = useDeferredValue(query);
  const deferredCategory = useDeferredValue(category);

  const filteredProducts = useMemo(() => {
    if (!deferredQuery && deferredCategory === 'all') {
      return products; // Return all products if no filters
    }

    return products.filter((product) => {
      const matchesQuery =
        deferredQuery === '' ||
        product.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
        product.description.toLowerCase().includes(deferredQuery.toLowerCase());

      const matchesCategory = deferredCategory === 'all' || product.category === deferredCategory;

      return matchesQuery && matchesCategory;
    });
  }, [deferredQuery, deferredCategory]);

  const handleCategoryChange = (newCategory: string) => {
    startTransition(() => {
      setCategory(newCategory);
    });
  };

  return (
    <div className="product-search">
      <div className="search-controls">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search products..."
          className="search-input"
        />

        <select
          value={category}
          onChange={(e) => handleCategoryChange(e.target.value)}
          className="category-filter"
        >
          <option value="all">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
          <option value="books">Books</option>
        </select>
      </div>

      <Suspense fallback={<div>Loading...</div>}>
        <ProductList products={filteredProducts} isStale={isPending || query !== deferredQuery} />
      </Suspense>
    </div>
  );
}

function ProductList({ products, isStale }: { products: Product[]; isStale: boolean }) {
  return (
    <div className={`product-list ${isStale ? 'stale' : ''}`}>
      {products.map((product) => (
        <div key={product.id} className="product-card">
          <h3>{product.name}</h3>
          <p className="category">{product.category}</p>
          <p className="price">${product.price}</p>
        </div>
      ))}
    </div>
  );
}

This example combines several powerful patterns:

  • Deferred values for both search query and category filter
  • Transitions for category changes (since they’re typically less urgent than typing)
  • Visual feedback when results are stale (notice the isStale prop)
  • Memoized computation to prevent unnecessary filtering

Advanced Pattern: Cascading Deferrals

Sometimes you have multiple levels of expensive computation. Here’s a pattern for handling cascading deferrals:

function DataDashboard() {
  const [dateRange, setDateRange] = useState({ start: '', end: '' });
  const [filters, setFilters] = useState<FilterOptions>({});

  // First level: defer the raw data fetching
  const deferredDateRange = useDeferredValue(dateRange);

  const rawData = useMemo(() => {
    return fetchAnalyticsData(deferredDateRange);
  }, [deferredDateRange]);

  // Second level: defer the filtering of that data
  const deferredFilters = useDeferredValue(filters);

  const filteredData = useMemo(() => {
    return applyFilters(rawData, deferredFilters);
  }, [rawData, deferredFilters]);

  // Third level: defer the expensive chart calculations
  const chartData = useMemo(() => {
    return generateChartData(filteredData);
  }, [filteredData]);

  return (
    <div>
      <DateRangePicker value={dateRange} onChange={setDateRange} />
      <FilterControls value={filters} onChange={setFilters} />
      <ExpensiveChart data={chartData} />
    </div>
  );
}

Each level of deferral protects the more urgent updates above it. Date range changes won’t block filter updates, and filter changes won’t block the chart rendering.

Handling Loading States

One challenge with useDeferredValue is managing loading states. Here’s a clean pattern for showing users when results are stale:

function SearchWithLoadingState() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    // Simulate expensive search
    return performExpensiveSearch(deferredQuery);
  }, [deferredQuery]);

  // Key insight: results are "stale" when the current query
  // doesn't match the deferred query
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />

      <div className="results-container">
        {isStale && <div className="loading-overlay">Searching...</div>}
        <div className={isStale ? 'results stale' : 'results'}>
          {results.map((result) => (
            <div key={result.id}>{result.title}</div>
          ))}
        </div>
      </div>
    </div>
  );
}

The isStale check is your friend—it tells you when the UI is showing outdated results while new ones are being computed.

When NOT to Use useDeferredValue

Not every expensive operation benefits from useDeferredValue. Here are some cases where it’s not the right tool:

// ❌ Don't defer critical user feedback
function LoginForm() {
  const [email, setEmail] = useState('');
  const deferredEmail = useDeferredValue(email); // Bad idea!

  const validationErrors = useMemo(() => {
    return validateEmail(deferredEmail);
  }, [deferredEmail]);

  // User expects immediate validation feedback
  return (
    <div>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {validationErrors.map((error) => (
        <div key={error}>{error}</div>
      ))}
    </div>
  );
}

// ❌ Don't defer simple computations
function SimpleCounter() {
  const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count); // Unnecessary overhead

  return <div>Count: {deferredCount}</div>;
}

// ✅ DO use it for expensive, non-critical updates
function AnalyticsDashboard() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const expensiveStats = useMemo(() => {
    return calculateComplexAnalytics(deferredQuery);
  }, [deferredQuery]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ComplexChart data={expensiveStats} />
    </div>
  );
}

Combining with Concurrent Features

useDeferredValue works beautifully with React’s other concurrent features:

function ModernSearchApp() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    return searchDatabase(deferredQuery);
  }, [deferredQuery]);

  const handleQueryChange = (newQuery: string) => {
    setQuery(newQuery); // High priority - updates immediately

    // Low priority updates can be wrapped in transitions
    startTransition(() => {
      updateRecentSearches(newQuery);
      logSearchAnalytics(newQuery);
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleQueryChange(e.target.value)}
        placeholder="Search..."
      />

      <Suspense fallback={<SearchSkeleton />}>
        <SearchResults results={results} isLoading={isPending || query !== deferredQuery} />
      </Suspense>
    </div>
  );
}

Performance Tips

Here are some practical tips for getting the most out of useDeferredValue:

Measure Before Optimizing

// Use React DevTools Profiler to identify actual bottlenecks
function ExpensiveComponent({ data }: { data: string }) {
  const deferredData = useDeferredValue(data);

  const result = useMemo(() => {
    console.time('expensive-computation');
    const result = doExpensiveWork(deferredData);
    console.timeEnd('expensive-computation');
    return result;
  }, [deferredData]);

  return <div>{result}</div>;
}

Granular Deferrals

Sometimes it’s better to defer specific parts rather than entire objects:

// ❌ Defers the entire filter object
const deferredFilters = useDeferredValue(filters);

// ✅ Defer only the expensive parts
const deferredSearchTerm = useDeferredValue(filters.searchTerm);
const deferredCategory = useDeferredValue(filters.category);
// Keep sort order immediate since it's not expensive
const sortOrder = filters.sortOrder;

Custom Hooks for Common Patterns

Create reusable hooks for common deferral patterns:

function useDeferredSearch<T>(
  items: T[],
  query: string,
  searchFn: (items: T[], query: string) => T[],
) {
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    return searchFn(items, deferredQuery);
  }, [items, deferredQuery, searchFn]);

  const isStale = query !== deferredQuery;

  return { results, isStale };
}

// Usage
function ProductSearch() {
  const [query, setQuery] = useState('');
  const { results, isStale } = useDeferredSearch(products, query, (products, q) =>
    products.filter((p) => p.name.includes(q)),
  );

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ProductList products={results} isStale={isStale} />
    </div>
  );
}

Common Gotchas

Gotcha #1: Missing Memoization

// ❌ Still re-renders on every change
function BadExample({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);

  // This runs on every render!
  const results = expensiveComputation(deferredQuery);

  return <div>{results}</div>;
}

// ✅ Properly memoized
function GoodExample({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    return expensiveComputation(deferredQuery);
  }, [deferredQuery]);

  return <div>{results}</div>;
}

Gotcha #2: Deferring the Wrong Thing

// ❌ Deferring the final result instead of the input
function BadExample({ items, query }: { items: Item[]; query: string }) {
  const filteredItems = items.filter((item) => item.name.includes(query));
  const deferredItems = useDeferredValue(filteredItems); // Wrong!

  return <ItemList items={deferredItems} />;
}

// ✅ Defer the input, memoize the computation
function GoodExample({ items, query }: { items: Item[]; query: string }) {
  const deferredQuery = useDeferredValue(query);

  const filteredItems = useMemo(() => {
    return items.filter((item) => item.name.includes(deferredQuery));
  }, [items, deferredQuery]);

  return <ItemList items={filteredItems} />;
}

Wrapping Up

useDeferredValue is a powerful tool for building responsive UIs that handle expensive operations gracefully. The key principles to remember:

Last modified on .