Speed Up Your React Apps By using Debounce (with Typescript)
Jasser Mark Arioste
If you're calling the search API for every input onChange
event, not only would your app feel slow, it also fetches unnecessary data due to the repeated API calls. Using debounce is one of the techniques you should learn to speed up your apps by limiting function executing on user-generated input (e.g., onChange
events). In React, this translates to limiting the re-rendering of your component.
In this tutorial, we'll cover how to implement debounce in react functional components. We'll start on how to implement debounce without react, why this doesn't work in react functional components, and move to different ways to implement debounce in functional components.
What is debouncing in JavaScript? #
In Ondrej Polesny's article about debounce, he stated the following: "In JavaScript, a debounce function makes sure that your code is only triggered once per user input. Search box suggestions, text-field auto-saves, and eliminating double-button clicks are all use cases for debounce". Debouncing means that we only execute the onChange
event handler when the user has finished typing, instead of executing it on every change.
Implement Debounce without React #
In the olden days (before react hooks), we only had to use lodash.debounce then use the returned function to an onChange
input event. Take a look at the example below:
It's very simple and straight forward, we just wrap our handle with the lodash.debounce function and it takes care of limiting the function execution.
Why is it bad practice to use this technique in React? #
You might be thinking, "Oh I can use this technique in react" right? Well, you can but this is NOT a good practice. Every change in input will trigger a change in the state, which triggers a re-render. This also means that react will re-calculate the debounce function on every render. For example:
import { useState } from 'react'; import * as React from 'react'; import { debounce } from 'lodash'; export const DebouncedComponent = () => { const [val, setVal] = useState(''); const [values, setValues] = useState([]); const _handler = (e: React.ChangeEvent<HTMLInputElement>) => { const newVal = e.target.value; setVal(newVal); setValues([...values, newVal]); }; //will get calculated in each render, so the timer gets reset. const debouncedHandler = debounce(_handler, 500); return ( <React.Fragment> <h2>Using Debounce + Handler directly</h2> <input type="text" value={val} onChange={debouncedHandler} /> <div> {values.map((val, index) => { return <li key={index}>{val}</li>; })} </div> </React.Fragment> ); };
123456789101112131415161718192021222324252627
Like the example without react, this should print the debounced values after the user has finished typing right? Nope. It only prints an empty string. It doesn't work since we're using a controlled component.
In a related article, Dimitri solves this problem by using a useMemo or useCallback.
To me, this seems to be the wrong approach.
React is all about states. We should debounce the state rather than creating a debounce handler. We can do this by implementing a useDebounce
hook.
Implementing the useDebounce
hook
#
Implementation for useDebounce
hook is simple. See the code below:
import { useEffect, useState } from 'react'; function useDebounce<T>(value: T, delay?: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { //create a timer to delay setting the value. const timer = setTimeout(() => setDebouncedValue(value), delay || 500); //if the value changes, we clear the timeout and do not change the value return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; } export default useDebounce;
1234567891011121314151617181920
It follows the principles of debouncing. We delay function execution. But if there's a change in the value, we reset the timer and delay it again until there are no more updates.
To use the useDebounce
hook it's very simple:
import { useState } from 'react'; import * as React from 'react'; import useDebounce from './useDebounce'; export const DebouncedComponent = () => { const [val, setVal] = useState(''); //use debounce here by observing val const debouncedVal = useDebounce(val, 500); const [values, setValues] = useState([]); const _handler = (e: React.ChangeEvent<HTMLInputElement>) => { const newVal = e.target.value; setVal(newVal); }; //only set the values when debouncedVal changes and print them. React.useEffect(() => { setValues([...values, debouncedVal]); }, [debouncedVal]); return ( <React.Fragment> <h2>Using Debounce + Handler directly</h2> <input type="text" value={val} onChange={_handler} /> <div> {values.map((val, index) => { return <li key={index}>{val}</li>; })} </div> </React.Fragment> ); };
12345678910111213141516171819202122232425262728293031
Demo:
This is definitely a much cleaner way to handle debounce in React. We keep the synthetic onChange
event handler. We still re-render the component on every input change. However, we only print the debounce values after the user has finished typing, or in your case call the backend API.
NOTE: We don't have to implement it ourselves. We can install usehooks-ts package fully supported with typescript and use the hook from there.
Implementing the useDebounceEffect
hook
#
The useDebounce
hook is useful for observing and delaying updates to a single state. What if we have multiple states to observe and just want execute a function just like useEffect
?
We can implement a useDebounceEffect
hook for this use case.
//useDebounceEffect.tsx import { useEffect, useCallback, DependencyList } from 'react'; function useDebounceEffect(effect: any, deps: DependencyList, delay = 250) { const callback = useCallback(effect, deps); useEffect(() => { const timeout = setTimeout(callback, delay); return () => clearTimeout(timeout); }, [callback, delay]); } export default useDebounceEffect;
1234567891011121314
We can further simplify our previous example with this hook:
import { useState } from 'react'; import * as React from 'react'; import useDebounceEffect from './useDebounceEffect'; export const DebouncedComponent = () => { const [val, setVal] = useState(''); const [values, setValues] = useState([]); const _handler = (e: React.ChangeEvent<HTMLInputElement>) => { const newVal = e.target.value; setVal(newVal); }; //directly use val but just use the useDebounceEffect hook. useDebounceEffect(() => { setValues([...values, val]); }, [val]); return ( <React.Fragment> <h2>Using useDebounce hook</h2> <input type="text" value={val} onChange={_handler} /> <div> {values.map((val, index) => { return <li key={index}>{val}</li>; })} </div> </React.Fragment> ); };
123456789101112131415161718192021222324252627282930
Demo:
Conclusion #
Thank you for reaching this far! So far, we've learned about debounce in javascript, how to implement it the old way, and how to implement it in react. We're now able to speedup our react apps by executing our functions efficiently.
If you found this tutorial helpful, please leave a like or share! If you'd like more tutorials like this in the future, be sure to subscribe to our newsletter or follow me on twitter!
Resources #
usehooks-ts docs - https://usehooks-ts.com/react-hook/use-debounce
Credits: Image by David Mark from Pixabay