Adding a useDebounce React Hook

Learn how to optimize and reduce application rerenders from React onChange events via a custom useDebounce hook.

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.

Press + to interact
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.

Press + to interact
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 ...