Nearly 90% of websites require search functionality, making it a crucial element for enhancing user experience. The efficiency of this search functionality is equally important, and in this blog, we will explore how to optimize it using a rate limiter concept. By delving into the concept of D********g, we can significantly improve the search experience for users. Let's dive right in!
The design is straightforward: it consists of an input field for searching, and depending on the user's input, a list will be rendered.
However, utilizing the conventional approach for this functionality has its drawbacks. In this blog, we will delve into the disadvantages of the traditional method and propose an alternative solution to overcome these challenges. Let's proceed to explore these issues and find a more efficient way to implement the search functionality.
Search box Component....
This is our search box component, comprising two components: a Textfield from the Material-UI library, responsible for capturing user input, and a list of div elements that render the matching values based on the input provided. With this setup, users can input their search queries, and the list will dynamically display relevant items that match the search criteria.
arr - holds the facts about programming
In this search box component, we utilize React's state management to handle the dynamic behavior of the component.
let [ facts, setFacts ] = useState(arr): This line sets up the facts state variable, which initially holds all the values from the arr. The setFacts function allows us to update the state as needed. By maintaining this state, we can dynamically modify and display the filtered list of values based on the user's search input.
let [ searchInput, setSearchInput ] = useState(""): Here, we create the searchInput state to keep track of the user's input in the search field. The setSearchInput function is used to update this state whenever the user types in the search box. With this state in place, we can filter the facts array against the searchInput value to display the matching items to the user.
With these state variables in the component, we can efficiently manage the input and filter the data, resulting in more interactive and responsive search functionality for the user.
We use useEffect to handle the side effects related to the searchInput state. By adding searchInput to the dependency array of useEffect, the effect will execute whenever the user types a new value into the Textfield. This is because the useEffect hook will be triggered whenever the value of searchInput changes.
The onChange callback of the Textfield is responsible for updating the searchInput state using setsearchInput(e.target.value). As the user types, this state gets updated with the current value of the input field.
Within the useEffect, we handle the filtering logic by calling a function, let's say searchFunctionality(searchInput). This function takes searchInput as a parameter and is responsible for updating the facts list based on the input.
The process works as follows: Whenever the user types a new value, the useEffect gets executed due to the change in the searchInput state. This triggers the searchFunctionality with the updated searchInput, which in turn filters the data and updates the facts list accordingly. As a result, the list displayed to the user gets dynamically updated to show the matching items based on the search input.
searchFunctionality explained.
In the searchFunctionality, we create a regular expression (regex) based on the searchInput. For this purpose, we use the RegExp constructor, setting the i flag to make the pattern case-insensitive. The regex pattern will be created as follows: let pattern = new RegExp(searchInput, "i").
Next, we utilize the filter method on the arr to iterate through each value (d) in the array. For each value, we test whether it matches the regex pattern using pattern.test(d). The test method returns a boolean value, indicating whether the pattern is found in the value.
If the test method returns true for a specific value d, it means that the searchInput matches that particular value. In such a case, we include that value in the result array.
After iterating through all the values in the arr, the result array will contain only those items that match the searchInput.
Finally, we update the state of facts using setFacts(searchValue), where searchValue represents the filtered array containing only the matching items. This causes the component to re-render with the updated list, displaying only the items that match the user's search query.
Time to see the output.....
if you closely observe the logs it is evident that we are trying to search the arr list with every input the user types,
It will be fine for some small list of values but what if the list grows
In most cases, users typically search for specific words rather than individual letters. Therefore, there is no need to trigger a search operation for each letter they type. Instead, it would be more efficient to perform the search based on complete words or phrases
we can optimize the search functionality by employing rate-limiting techniques
rate-limiting?
limiting the number of times the user can call a function (searchFunctionality) attached to an event (onChange of TextField)
The two most popular rate-limiting techniques are Debouncing and Throttling
Here we will be using debouncing for this functionality and we will be also discussing throttling in our upcoming blogs
What is debouncing?
Debouncing is a programming practice used to ensure that time-consuming tasks do not fire so often, that it stalls the performance of the web page. In other words, it limits the rate at which a function gets invoked
This implementation allows us to execute the searchFunctionality when the user finishes typing the word.
but how do we know? when the user finishes typing a word?
Let's first understand how debouncing works, then it will be cleared
Here is the debouncing implementation . . . .
Here, we define a function called debouncing that takes two parameters: func and limit.
func is the function that we want to debounce. It's the function that we want to delay and execute only after a certain period of inactivity.
limit is the time duration (in milliseconds) that represents the delay period. It indicates how long we want to wait before executing the func after the last call.
This debouncing function will return a closure function which has access to the timer variable and will execute the func we passed, based on the limit
Now, we will store the debounced version of our searchFunctionality by passing the function as an argument to the debouncing and followed by the limit (we will be using limit as 300 )
let debouncFunc = useCallback(debouncing(searchFunctionality, 300), []);
instead of calling the searchFunctionality directly in the useEffect we call the debouncFunc in the useEffect which now holds the debounce implementation for the functionality (searchFunctionality) we have passed
just remember two things from the closure function returned by the debouncing it stores an
setTimeout in the timer variable which only executes after the limit
clearTimeout(timer) to clear the previously stored setTimeout.
But how it works let's understand this by dry run
now user types an input 'h'
now the flow will be onChange -> updated searchInput state -> triggers useEffect -> calls debouncFunc
this is the first time user typed an input so there is no previous timeout to get cleared and the new timeout for the instance created and stored in timer for the input 'h' which will get executed after the limit means the searchFunctionality will execute after 300ms .
now user types an input 'e'
this event occurs within the limit we mentioned (300)
now again the flow will be onChange -> updated searchInput state -> triggers useEffect -> calls debouncFunc
this is a second event for the same input and also occured within the mentioned limit the previously stored timer (for 'h' instance) is cleared and new timer instance is stored for the event 'he' and now this will wait for again 300 ms(limit) to get executed
assume that 'l', 'l', 'o' executed within 300 respectively all other previously created timer will be cleared e.g.
'he' timer cleared on the event of 'hel' and 'hel' will be cleared on the event of 'hell' and lastly 'hell' timer will be cleared on the event of 'hello'
now the timer will have an instance for the input 'hello' and
now user types an input 'w'
this event occured after 500 ms from the 'o'
now again the flow will be onChange -> updated searchInput state -> triggers useEffect -> calls debouncFunc
now if you observe closely this event occured after 500 ms from that of previous which indicates that the timer instance stored for 'hello' have executed the searchFunctionality which updates the facts list based on the searchInput and render the UI with new values because the limit passed is 300 which is lesser than the 500 of the next event.
like this the execution of the actual functionality(searchFunctionality) is limited using the debounce method.
Note: if it seems overwhelming please gothrough it again you will eventually get it
let me know in the comments why we used the useCallback hook
the upadted code and the output is
To make it more interactive I have attached the codesandbox so that you can paly around with the searchbox
Conclusion
In conclusion, debouncing is a powerful technique that significantly enhances the performance and user experience of web applications. By introducing a delay before executing functions, we can avoid unnecessary computations and reduce network traffic, resulting in a more efficient and responsive application. Stay tuned for the next section, where we will explore using Promises to return values from debounced functions, empowering you to take your debouncing skills to the next level. Happy coding!