Back to all notes

useDeferredValue: Keep Input Snappy During Slow Renders

Defer updating slow results so typing stays responsive.

2 min read

I had a search box where every keystroke re-filtered a huge list, and the input lagged because the keystroke and the expensive render ran at the same priority. useDeferredValue separates them: the input updates immediately, and the slow list renders when the browser has time for it.

The version that stutters

The raw query flows straight into SlowList, so the 10,000-item filter runs on every keypress and typing stutters.

import { useState } from "react";

function SlowList({ query }) {
  const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
  const filtered = items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
  
  return (
    <ul>
      {filtered.slice(0, 100).map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

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

  return (
    <div>
      <h1>useDeferredValue</h1>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <SlowList query={query} />
    </div>
  );
}

export default App;

Deferring the query

The input stays bound to query. The list gets a deferred copy instead:

import { useState, useDeferredValue } from "react";

function SlowList({ query }) {
  const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
  const filtered = items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
  
  return (
    <ul>
      {filtered.slice(0, 100).map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

function App() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      <h1>useDeferredValue</h1>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <SlowList query={deferredQuery} />
    </div>
  );
}

export default App;

useDeferredValue(query) returns a value that trails the real one when renders are expensive. The input keeps updating at full priority, and the list re-renders with the older deferred value until the browser has time to catch up. If the slow child renders from the deferred value, wrap it in memo as well, where appropriate, to avoid unnecessary work.

When I don’t defer

Critical UI stays out of this. Validation errors must appear the instant I type, not after the slow render finishes. And I measure before and after, deferring only what’s actually slow.