Table of contents
What is debouncing
Debouncing is a technique or a pattern that can be used to solve commonly occurring problems like grouping multiple side effects of an event into a single effect.
The original idea comes from electronics like when we press a button on a remote,
once a signal from the button is received, the microchip stops processing signals from the button for a few microseconds while itβs physically impossible for you to press it again.
Use cases
Autosaving feature in text editors
Suppose we have a text editor which helps the user to type something and save the data to the backend.
Setup
import * as React from "react";
import "./styles.css";
export default function App() {
const [content, setContent] = React.useState<string>("");
const [dbState, setDbState] = React.useState<string>("");
// database call to persist the content
const saveToDb = (data: string) => setDbState(data);
const onContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
setContent(value);
saveToDb(value);
};
return (
<div className="App">
<h1>Text Editor example</h1>
<textarea
name="content"
id="content"
cols={40}
rows={10}
className="editor"
placeholder="type here"
value={content}
onChange={onContentChange}
></textarea>
<div className="states-container">
<div className="state-container">
<h4>π» Client State</h4>
<span>{content}</span>
</div>
<div className="state-container">
<h4>ποΈ Database State</h4>
<span>{dbState}</span>
</div>
</div>
</div>
);
}
So what's happening?
We have a simple textarea acting as an editor in any app. And we have a state i.e. content
that we have hooked with the textarea, standard react nothing fancy.
But we have another state dbState
which is representing our database state in the backend.
As the user starts typing we see our content
state being updated and displayed in the bottom left, and right after that, we call setDbState
to update the database state, which is basically simulating an API call to any remote server and that is being displayed on the bottom right
Preview
Hmmm π€
Problem
I am sure you can see the core problem here. We are basically making an API call to save the content every time the user types something. This may put a lot of unnecessary load on your backend server depending on your app popularity and usage. Here the side effects are multiple calls to the backend server for saving data and the event is user typing.
So how do we go about solving this?
Well if we think about it,
we have to somehow know if the user has paused typing for some time, and during that time we call our API to save data in bulk instead of every keypress.
or the other way of saying this is
if the user has not typed anything for let's say
t
seconds, we will call the API to save the data. Before that, we need to somehow ignore the previous API calls.
Let's implement a function that can do this
export function debounce(func: (args: any) => void, timeout: number = 300) {
let timer: number;
return (...args: any) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}
So what's happening ?
Basically, we have a function debounce
that takes 2 arguments
func
: the function that we want to calltimeout
: after how many seconds we are going to call the passes function
- We first create a timer using the
let
keyword because we need to override this. - Then we return a function, we destructured all the arguments the function may receive
- We clear the timer using
clearTimeout
If we have any. (maybe from the previous call) - We start a new timer using
setTimeout
and inside the callback, we call ourfunc
and pass the context and arguments.
Let's use this in our app
// database call to persist the content
const saveToDb = (data: string) => setDbState(data);
// debounced version of saveToDb
const debouncedSaveToDb = debounce(saveToDb, 1000);
const onContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
setContent(value);
debouncedSaveToDb(value);
};
We created a debounced version of our saveToDb
function. And now we calling deboundedSaveToDb
in our onContentChange
function whenever we change the editor content.
What to expect?
When we start typing our client state i.e content
should update and reflect in the bottom left panel. But our database state i.e dbState
should wait for the user to stop typing for a certain period of time to update dbState
in one short and should be reflected in the bottom right panel.
Will this work?
hmmm π€
This is not what we expected right? The issue here is
instead of debouncing words and sentences we are debouncing each character as they are typed.
After typing a character our component is triggering the deboundedSaveToDb
which internally calls saveToDb
with the typed character.
Why is this happening?
The root cause is react re-rendering, Let me explain
Whenever a user types a character our content
state changes and react will re-render the entire component which is normal. But we will also get a fresh deboundedSaveToDb
function on every re-render hence the debouncing effect will not work because the timer will be different for each character typed.
So what's the solution?
We have to stop creating fresh deboundedSaveToDb
on every re-render. This should be only created once, basically we want to memoize deboundedSaveToDb
function. And we have useCallback
for the same in react.
Let's see how we can modify our code to fix this
// database call to persist the content
const saveToDb = (data: string) => setDbState(data);
// memoized debounced version of saveToDb
const debouncedSaveToDb = React.useCallback(debounce(saveToDb, 1000), [ ]);
As we can see we are using useCallback
to tell react that only re-create debouncedSaveToDb
when these dependencies changes i.e [ ]
, We can see that the dependency array is empty which implicitly means only create this once.
Result
π working
As we can see we are perfectly batching all the changes and triggering saveToDb
when user stopped typing for a certain period of time.
We can also achieve the desired result using ref
We can use useRef
to store debouncedSaveToDb
after creating and this will make sure it does not get affected when react re-renders the component.
// database call to persist the content
const saveToDb = (data: string) => setDbState(data);
// memoized debounced version of saveToDb
// const debouncedSaveToDb = React.useCallback(debounce(saveToDb, 1000), []);
const debouncedSaveToDb = React.useRef(debounce(saveToDb, 1000)).current;
The result will be similar to the useCallback
approach.