When it comes to building a React app that requires input from users, I constantly urge myself to take the time to think about how debouncing logic may be applicable for components that require rapid state updates. For large-scale React apps, memory usage and performance optimization issues become incredibly important topics. A React app can contain tens if not hundreds of unique components that track and update a plethora of local state values and the less we have to trigger updates to these states while still maintaining up-to-date values the better.
Debouncing in React, at a high level, is a development tool one can use to greatly improve browser performance by restricting how often a state value update function is executed. A few straightforward examples of React components that can benefit from debouncing implementations would be a string/text input, a slider switch where a user may rapidly scroll back & forth until reaching their desired selection, or even something as basic as a group of radio buttons where a user can swiftly select and deselect available options.
A simple scenario we can use to showcase how debouncing can improve browser performance is to think of a basic user login page. For this example, we’ll create two string inputs, a “username” input, as well as an “email” input. The rudimentary approach for keeping track of character changes within these two inputs would be to instantiate a state object (we'll create a formValues state object for our demo) in which we will store the up-to-date values using individual properties within the state object. After every keystroke a user makes for a corresponding input, we'll call our state update function via input element's onChange event. If we take a step back to think about the increasing number of keyboard warriors in the modern world, we can confidently assume that a majority of users will be able to type in their desired credentials very swiftly. Do we really need to keep track of every individual keystroke they make? Or does it make more sense to update our state object storing these input values once a user has finished typing?
Lets play around with an example where I demonstrate this kind of basic implementation of storing up-to-date input values of two form inputs using a single local state object without debouncing involved. Spend a moment typing into either of the string inputs and keep an eye on the "# of state updates occured" count near the bottom. You'll want to click over to the right-most file debouncingExample.js in the sandbox header file navigation to view the relevent React code.
For every unique character entered (or deleted) by a user, our setFormValues() state update function is executed via the onChange event. For a simple example like this one, the quantity of state updates will not cause any sort of noticable performance issues since its the only state logic that exists. If we picture a large-scale React application where a single screen can contain a countless number of unqiue componets that will require several rerenders from local & global state updates alongside several executions of logic functions, updating a state value for every individual keystroke is far from ideal.
What if instead we could update our state values when a user has stopped typing, rather than every time a character change is made? To answer the question, we can! While this might sound like a time-consuming implementation for new or existing componenets, it can be done with merely a few lines of code. Utilities we'll need to leverage for effortlessly implementing debounce functionality include two important aspects: First we'll use the useCallback hook, available directly from the React library. This specific hook is important because even though our handleChange() function is recreated on each render, we want to now reference the same memoized function that we created on the initial page load. Second, we'll use Lodash's debounce method which delays the invoking of a function until after a specifically provided number of milliseconds has elapsed since the last time the debounced function was invoked. This function will be placed in our new updated handle function, which we'll call debounceSetData(). Below we can look at the before and after code for our handleChange() function.
const handleChange = (input, inputName) => { const newFormValues = { ...formValues }; newFormValues[inputName] = input; setFormValues(newFormValues); };
→
const handleChange = (input, inputName) => { const newFormValues = { ...formValues }; newFormValues[inputName] = input; debounceSetData(newFormValues); }; const debounceSetData = useCallback( debounce((formValues) => { setFormValues(formValues); }, 250), [] );
250 milliseconds is a somewhat-standard time value to tell debounce it's time to update our state values for a string input, but this time delay option can vary case-by-case based on the type of user input. I can't speak highly enough about the Lodash package and not only about its debounce function, but the endless amount of utility functions it provides. It's worth the time to take a gander at the Lodash documentation to see all of the useful functions it provides (I'm always using merge, reduce, has, toArray, & isNil to name a few). While it certainly is possible to code your own version of a debouncing implementation, Lodash gives users the ability to import a debounce function as well as a massive collection of other utility functions that are battle-tested and reliable. Now lets take a peak at what integrating debouncing logic can do for performance in this example. Play around with typing characters in to the string inputs and continue to keep an eye on the "# of state updates occured" count. Again, head to debouncingExample.js for the good stuff.
You can immediately notice that when you type characters in to an input at a reletivley quick speed, our formValues state object won't fire an update until you've reached the 250ms threshold of not entering new characters. We can minimize the amount of state update executions by an incredible amount and in doing so, we dont have to perform nearly as many state update calls! In a large React app, reducing the amount of state update executions and proceeding rerenders is a beautiful optimization. Considering how simple debouncing is to implement via Lodash, this implementation is something that I love.
Debouncing is an incredibly powerful tool. From my React development experience thus far I feel as though it doesn't get nearly enough attention in terms of its benefits. Almost all web apps built using React & mobile apps built using React Native will eventually run into performance related struggles as an app piles on new memory-intensive features. The quantity of state data updates grows expeonentially as a React app's codebase grows in size. Debouncing is a wonderful feature that can be implemented in several use cases to get the most out of your app's general performance.
If you have any insights, comments, or questions about debouncing feel free to reach out to me via the contact section below. Thanks for reading!
Tucker Massad
Boston, MA
tuckermassad@gmail.com