Skip to the code
cf.

useDebouncedValue

Write useDebouncedValue(value, delay) and export it from useDebouncedValue.js. The tea shop's search box gets a new value on every key press, but should only search once the customer pauses. Your hook returns a copy of value that lags behind: it catches up once value has stayed the same for delay milliseconds.

On the first render it returns value straight away. After that, each change starts the wait again, so while the customer keeps typing it keeps returning the value from before they started. A value that was replaced before its wait ended is never returned. Renders that don't change value don't restart the wait.

Leave no timers behind: at most one is waiting at a time, and removing the component clears it.

function TeaSearch() {
  const [query, setQuery] = useState("");
  const search = useDebouncedValue(query, 300);
  // Show query in the box, and search for search.
}

The Preview shows Demo.jsx, a search box that uses your hook.

What the tests check

  1. returns the first value straight away
    useFakeTimers();
    const { result } = renderHook(() => useDebouncedValue("g", 300));
    expect(result.current).toBe("g");
    
  2. catches up delay ms after a change
    const clock = useFakeTimers();
    const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 300), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    expect(result.current).toBe("g");
    await clock.advance(299);
    expect(result.current).toBe("g");
    await clock.advance(1);
    expect(result.current).toBe("gr");
    
  3. each change starts the wait again
    const clock = useFakeTimers();
    const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 300), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    await clock.advance(200);
    rerender({ value: "gre" });
    await clock.advance(200);
    expect(result.current).toBe("g");
    await clock.advance(100);
    expect(result.current).toBe("gre");
    
  4. renders that don't change the value don't restart the wait
    const clock = useFakeTimers();
    const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 300), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    await clock.advance(200);
    rerender({ value: "gr" });
    await clock.advance(100);
    expect(result.current).toBe("gr");
    
  5. uses the delay it's given
    const clock = useFakeTimers();
    const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 1000), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    await clock.advance(999);
    expect(result.current).toBe("g");
    await clock.advance(1);
    expect(result.current).toBe("gr");
    
  6. keeps only one timer waiting
    const clock = useFakeTimers();
    const { rerender } = renderHook(({ value }) => useDebouncedValue(value, 300), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    await clock.advance(100);
    rerender({ value: "gre" });
    await clock.advance(100);
    rerender({ value: "gree" });
    expect(clock.pending()).toBe(1);
    
  7. removing the component clears its timer
    const clock = useFakeTimers();
    const { rerender, unmount } = renderHook(({ value }) => useDebouncedValue(value, 300), { initialProps: { value: "g" } });
    rerender({ value: "gr" });
    unmount();
    expect(clock.pending()).toBe(0);
    
  8. in a component, searches once typing stops
    function TeaSearch() {
      const [query, setQuery] = React.useState("");
      const search = useDebouncedValue(query, 300);
      return (
        <div>
          <label>
            Search teas <input value={query} onChange={(event) => setQuery(event.target.value)} />
          </label>
          <p>Searching for: {search}</p>
        </div>
      );
    }
    const clock = useFakeTimers();
    render(<TeaSearch />);
    await user.type(screen.getByLabelText("Search teas"), "oolong");
    expect(screen.getByText("Searching for:")).toBeInTheDocument();
    await clock.advance(300);
    expect(screen.getByText("Searching for: oolong")).toBeInTheDocument();
    

Learn more

useDebouncedValue.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).