Search⌘ K
AI Features

Adding a useDebounce React Hook

Explore how to create and apply a custom useDebounce React hook to limit the frequency of search query updates. Understand debouncing concepts, hook lifecycle management, and improve your app's responsiveness by reducing excess renders during rapid input changes.

While everything with our generic search function is working well, we can optimize how the SearchInput function calls the setSearchQuery callback.

Monitor renderings

Let’s investigate what our search function currently does by adding a console.log() call to the onChange event within SearchInput.

TypeScript 3.3.4
import * as React from "react";
export interface ISearchInputProps {
setSearchQuery: (searchQuery: string) => void;
}
export function SearchInput(props: ISearchInputProps) {
const { setSearchQuery } = props;
return (
<>
<label htmlFor="search" className="mt-3">
Search! Try me!
</label>
<input
id="search"
className="form-control full-width"
type="search"
placeholder="Search..."
aria-label="Search"
onChange={(event) => {
// Monitor when this event fires
console.log("Firing!");
setSearchQuery(event.target.value)
}}
/>
</>
);
}

Likewise, add a console.log() that says Rendering! to App.tsx.

Now, when we run our app and look in the browser console, we should see a Rendering! log for every Firing! log.

Shell
Firing!
Rendering!
Firing!
Rendering!
Firing!
Rendering!
...

With some modifications, we can simulate the console.log messages by appending them right into our chat. We can check out the application in action here and see how every keystroke entered in the search field causes a rerender.

import * as React from 'react';
import { useState } from 'react';
import { widgets } from './mock-data/widgets';
import { people } from './mock-data/people';
import { genericSearch } from './utils/genericSearch';
import { SearchInput } from './components/SearchInput';

export default function App() {
  const [query, setQuery] = useState("");
  const [consoleLogs, setConsoleLogs] = useState<Array<string>>([])
  console.log("Rendering!")

  return (
    <>
      <p>Simulated Console Output:</p>
      <pre style={{height: "100px", overflowY: "scroll"}}>{consoleLogs.join("\n")}</pre>
      <SearchInput setSearchQuery={setQuery} onConsoleLog={newLogs => setConsoleLogs([...consoleLogs, ...newLogs])}/>
      <h2>Widgets:</h2>
      {widgets.filter((widget) => genericSearch(widget, ["title", "description"], query, false)).map(widget => {
        return (
          <h3>{widget.title}</h3>
        )
      })}
      <h2>People:</h2>
      {people.filter((person) => genericSearch(person, ["firstName", "lastName", "eyeColor"], query, false)).map(person => {
        return (
          <h3>{person.firstName} {person.lastName}</h3>
        )
      })}
    </>
  )
}
App with simulated console.log() output

This is where we have a performance optimization opportunity. It’s suboptimal to rerender our search, which includes refiltering our entire widgets and people lists, immediately after each keystroke from the user. Many people type quite quickly ...