The article discusses using Lodash's debounce and throttle functions in a React application with hooks.
Abstract
The article begins by explaining that Lodash is a built-in library in Create React App and provides utility functions for arrays, numbers, objects, and strings. The author then introduces the debounce and throttle functions in Lodash, explaining their purpose and usage. The article goes on to demonstrate how to use these functions in a React application with hooks, such as useCallback, useMemo, and useRef. The author also discusses the benefits of using custom hooks to reuse programming logic and provides examples of how to create custom hooks for debounce and throttle. The article concludes by recommending the use of uncontrolled components for debounce and throttle use-cases.
Bullet points
Lodash is a built-in library in Create React App that provides utility functions for arrays, numbers, objects, and strings.
Debounce and throttle functions in Lodash are introduced for performance reasons, such as reducing the load of backend calls in response to user input.
The article demonstrates how to use debounce and throttle functions in a React application with hooks, such as useCallback, useMemo, and useRef.
Custom hooks can be created for debounce and throttle to reuse programming logic.
The use of uncontrolled components is recommended for debounce and throttle use-cases.
Lodash: Create React App’s Built-in Library for Debounce and Throttle With Hooks
Showcase debounce and throttle with useCallback, useMemo, useRef, and custom hooks
Following our 10 Fun Facts About Create React App, today we present the 11th fun fact about Create React App: It has built-in Lodash, a JavaScript library that provides utility functions for arrays, numbers, objects, and strings.
Although many functions can be replaced by ES2015+, Lodash is still a super set with extra handy utilities. We can take advantage of the built-in Lodash to explore debounce and throttle with hooks.
Lodash, Debounce, and Throttle
In our previous projects, Lodash was always a utility package to be installed. By running npm i lodash, the lodash package becomes part of dependencies in package.json.
This pattern changes with the Create React App. After invoking npx create-react-app my-app, Lodash is ready for use. There is no need to install it at all. lodash is not in package.json, but in package-lock.json, installed along with other packages.
Lodash can be imported as: import _ from “lodash”; and then used with underscore. Below are definitions and uses of debounce and throttle:
/**
* Creates a debounced function that delays invoking func until
* after wait milliseconds have elapsed since the last time the
* debounced function was invoked.
*/
_.debounce(func, [wait=0], [options={}])
/**
* Creates a throttled function that only invokes func at most
* once per every wait milliseconds.
*/
_.throttle(func, [wait=0], [options={}])
Lodash can also be imported individually and used without an underscore. In fact, this is the recommended way to allow Webpack’s tree shaking to create smaller bundles.
Why do we need debounce and throttle? They’re introduced for performance reasons. Take the user input as an example. If every keystroke invokes a backe nd call to retrieve information, we might crash the whole system. Instead, we give a wait time to reduce the load. debounce waits until a user stops typing for the wait duration, and then sends out an update request. throttle does it a little differently — it controls the update frequency under the wait throttle limit.
Let’s create a simple user interface to illustrate the concept.
In the above input field, a user types 123456. If the user listens to onChange and responses with console.log for every input value, these original values are 1, 12, 123, 1234, 12345, and 123456. Without debounce or throttle, it invokes six backend calls in a short moment.
In fact, a user may not care much about the intermediate results. What a user cares about is a final result for 123456 when typing stops. debounce would be the perfect choice for this case. It only processes the data when typing stops for a tick (wait time). When a user quickly types 123456, there is only one debounced value, 123456.
throttle works a little differently. A user may want a response in a controlled rate (wait time). By default, it prints out the first keystroke, 1. The other intermediate throttled values depend on the wait time and a user’s typing speed. But it is guaranteed that the final result, 123456, will be outputted.
The following is a sample output if we put original values, debounced values, and throttled values together.
Original Value =1
Throttled Value =1
Original Value =12
Original Value =123
Original Value =1234
Original Value =1234
Original Value =12345
Original Value =123456
Throttled Value =123456
Debounced Value =123456
The Wrong Approaches
Let’s implement the input example with debounce and throttle in the Create React App environment. src/App.js is revised as follows:
Run npm start and quickly type 123456 in the input field. The console shows this result:
Original Value =1
Throttled Value =1
Original Value =12
Throttled Value =12
Original Value =123
Throttled Value =123
Original Value =1234
Throttled Value =1234
Original Value =12345
Throttled Value =12345
Debounced Value =1
Original Value =123456
Throttled Value =123456
Debounced Value =12
Debounced Value =123
Debounced Value =1234
Debounced Value =12345
Debounced Value =123456
Both debounce and throttle print out every keystroke change. What happened?
React re-render is caused by changes to state or props. In the above approach, onChange triggers handleInputChange (lines 8-18) when a user types a keystroke. Line 11 sets a new state value, which causes a new render to display the value (line 22).
For every keystroke, a new debounce function (lines 12-14) and a new throttle function (lines 15-17) are generated. That’s why they simply debounce and throttle every value.
The Correct Approaches
In order to make debounce and throttle behave correctly, we need to create the functions that would be memoized between renders. Memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
useCallback is a good candidate. It returns a memoized version of the callback. This function only changes if one of the dependencies has changed. useCallback(fn, deps) conditionally preserves the function, fn.
Here is the src/App.js that applies useCallback to memoize debounce and throttle functions:
At lines 8-13, debounceHandler is the memoized debounce function by useCallback. Since it has an empty dependency array, it is preserved for the full lifetime of the component.
At lines 15-20, throttleHandler is the memoized throttle function by useCallback. Since it has an empty dependency array, it’s preserved for the full lifetime of the component.
Now, this approach works.
While useCallback returns a memoized callback, useMemo returns a memoized value. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps), where the function is memoized as a value.
We make use of useMemo in src/App.js:
At lines 8-14, debounceHandler is the memoized debounce function by useMemo. Since it has an empty dependency array, it’s preserved for the full lifetime of the component.
At lines 16-22, throttleHandler is the memoized throttle function by useMemo. Since it has an empty dependency array, it is preserved for the full lifetime of the component.
This approach also works.
In Everything You Want to Know About React Refs, we gave a detailed description of useRef. It returns a mutable ref object whose .current property is initialized to the passed argument. The returned object will persist for the full lifetime of the component.
We can also employ useRef to memoize debounce and throttle functions in src/App.js:
At lines 8-13, debounceHandler is initialized by debounce function. It’s kept in the current value for the full lifetime of the component as it’s not reassigned. The invocation at line 26 needs to call on the current value.
At lines 13-18, throttleHandler is initialized by the throttle function. It’s kept in the current value for the full lifetime of the component as it’s not reassigned. The invocation at line 27 needs to call on the current value.
The Custom Hook Approaches
The previous approaches work. Are we going to build debounce or throttle handlers for every use-case?
Custom Hooks are a mechanism to reuse programming logic. Let’s see how to build the custom hooks for debounce and throttle.
For the sake of simplicity, we put custom hooks and usages in the same file. Ideally, they should be categorized as separate files. This is the revised src/App.js:
Lines 5-8 define a custom hook, useDebouncedCallback. It takes a callback and wait time, and then generates a debounce function accordingly. Since line 6 encloses it with useCallback and an empty dependency list, this debouncedFunction will not change for the full lifetime of the hook.
Lines 18-21 initialize useDebouncedCallback, which is used by line 33.
Lines 10-13 define a custom hook, useThrottledCallback. This takes a callback and wait time, and then generates a throttle function accordingly. Since line 11 encloses it with useCallback and an empty dependency list, this throttledFunction will not change for the full lifetime of the hook.
Lines 23-26 initialize useThrottledCallback, which is used by line 34.
This approach works with reusable custom hooks.
Instead of debouncing or throttling the callback, we can also write custom hooks to debounce or throttle values. These values can be programmed by callers for various usages.
This is the revised src/App.js:
Lines 5-9 define a custom hook, useDebouncedValue. It takes an initial value and a wait time. Internally, it keeps the original value and generates a debounce function for a debounced value. Since line 7 encloses it with useCallback and an empty dependency list, this debouncedFunction will not change for the full lifetime of the hook. This custom hook returns an array with the debounced value and the debounced function to update the value.
Line 19 initializes useDebouncedValue. debounceHandler is used by line 33 to update the value. Lines 33-35 listen to debouncedValue change, and print out the debounced value accordingly.
Lines 11-15 define a custom hook, useThrottledValue. It takes an initial value and a wait time. Internally, it keeps the original value and generates a throttle function for a throttled value. Since line 13 encloses it with useCallback and an empty dependency list, this throttledFunction will not change for the full lifetime of the hook. This custom hook returns an array with the throttled value and the throttled function to update the value.
Line 20 initializes useThrottledValue. throttleHandler is used by line 33 to update the value. Lines 37-39 listen to throttledValue change and prints out the throttled value accordingly.
The Uncontrolled Approaches
In all previous approaches, we use controlled components, which are recommended by the React team.
For the use-cases of debounce and throttle, it’s easier to use uncontrolled components.
Here’s the revised src/App.js for debounce:
Line 17 directly uses debounceHandler, which is defined at lines 10-13.
Similarly, we can revise src/App.js for throttle:
Line 17 directly uses throttleHandler, which is defined at lines 10-13.
Conclusion
The built-in Lodash in Create React App gives us the convenience of functional programming and manipulations of arrays, numbers, objects, and strings.
It’s fun to explore debounce and throttle usages, along with hooks — give it a try!
Thanks for reading, I hope it was helpful. You can see my other Medium publications here.