Welcome toWelcome to
  • home()
  • about()
  • projects()
  • blog()
;

PRANAY BAJRACHARYA

Link to my githubLink to my linkedinLink to my email
All blogs

Debounce Control: React component to debounce state

Nov 16, 2024 · 3 min read

Debounce Control: React component to debounce state

What is debounce? Debounce is a way to delay a function until an action stops. For example, in a search bar, instead of running a search every time you press a key, debouncing waits until you finish typing, then runs the search just once. This keeps the app fast by avoiding too many repeated actions.

But we already have a way to debounce state either using hook libraries like useDebounce or creating your own. So, why do we need this?

Let's step back and see how useDebounce work:

import React, { useState } from "react";
import { useDebounce } from "some-where";

const App = () => {
  const [text, setText] = useState("");

  const debouncedText = useDebounce(text, 1000); // 1000ms

  // do something with this 'debouncedText'

  return (
    <input 
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
    />    
  );
};

With each keystroke, we update the state with setText, and when the user stops typing for 1000ms, debouncedText finally updates. Ideally, this is when we'd make an API call.

Now, here's the problem: even though debouncedText only updates once, the text state still updates on each keystroke, causing the entire app to re-render on every input. This might not be an issue in this example, where we're just rendering a simple input element.

Let's take a real world example:

Bad Example

The code might look something like this:

import React, { useState } from "react";
import { useQuery } from 'react-query';
import { useDebounce } from "some-where";

const UserListPage = () => {
  const [state, setState] = useState(initialState);

  const debouncedState = useDebounce(state, 1000); // 1000ms

  const { data } = useQuery({
    queryKey: ['user', debouncedState],
    queryFn: () => fetchUserList(debouncedState),
  });

  return (
    <div>
      <UserFilter state={state} setState={setState} />
      <UserList data={data} />
    </div>
  );
};

export default UserListPage;

Now, do you see the problem? On each keystroke in any of the input fields, everything has to re-render, including the UserList component, even though nothing related to it has changed.

Now, this is where <DebounceControl /> comes into play.

import React, { useState } from "react";
import DebounceControl from "debounce-control";

const App = () => {
  const [text, setText] = useState("");

  const onDebouncedChange = (value) => {
    setText(value);
  }

  return (
    <DebounceControl
      value={text}
      delay={1000}
      onDebouncedChange={onDebouncedChange}
      render={({ value, onChange }) => (
        <input
          type="text"
          value={value}
          onChange={(e) => onChange(e.target.value)}
        />
      )}
    />
  );
};

export default App;

Notice how DebounceControl decouples the debouncing logic from the rest of the app. With this, the text state only updates after debouncing occurs, and in the previous example, the UserList or the entire component will no longer re-render on each keystroke.

Big question now. Why should we use this?
1. Minimal re-renders
2. Works with any form element (not limited to input or textarea). Also, the element is right there if you want to add a ref or modify props.
3. Type-safe 🤞

Demo:

Input component

Input component before DebounceControlInput component after DebounceControl

Slider component

Slider component with DebounceControl

Resizable component

Resizable component with DebounceControl

Installation:

NPM: https://www.npmjs.com/package/debounce-control

Conclusion:

By using <DebounceControl />, you can easily debounce any form element while keeping your app performant and type-safe.

Made with Love and Hardwork by Pranay Bajracharya
Designed and Maintained since 2022