DEV Community

Yogesh Chavan
Yogesh Chavan

Posted on • Edited on

How to use debouncing to Improve the performance of the search functionality

In this article, we will see a very powerful and must-use technique to improve the performance of search functionality in the application.

If we're making an API call to the server for every character typed in the input search box and the data returned by API contains a lot of data, let's say 500 or 1000 user records then it will slow down your application.

Because for every character typed in the search box, we're making an API call and the server may take some time to return data and before the server returns the data we're making another API call for the next character typed.

In almost every application we need to add some form of search functionality may be to filter some records or get the result from API.

So this is a common scenario and making an API call for every character typed may be expensive because some third-party applications or cloud providers like firebase, AWS, etc provide only a limited number of API requests, and cost is incurred for every additional request.

So to handle this scenario, we can use the debouncing functionality.

Let's first understand what is debouncing.

Debouncing allows us to call a function after a certain amount of time has passed. This is very useful to avoid unnecessary API calls to the server If you're making an API call for every character typed in the input text.

Let's understand this by writing some code.

Without Debouncing in class component

import React from 'react';
import axios from 'axios';
import { Form } from 'react-bootstrap';

export default class WithoutDebouncingClass extends React.Component {
  state = {
    input: '',
    result: [],
    errorMsg: '',
    isLoading: false
  };

  handleInputChange = (event) => {
    const input = event.target.value;

    this.setState({ input, isLoading: true });

    axios
      .get(`https://www.reddit.com/search.json?q=${input}`)
      .then((result) => {
        this.setState({
          result: result.data.data.children,
          errorMsg: '',
          isLoading: false
        });
      })
      .catch(() => {
        this.setState({
          errorMsg: 'Something went wrong. Try again later.',
          isLoading: false
        });
      });
  };

  render() {
    const { input, result, errorMsg, isLoading } = this.state;
    return (
      <div className="container">
        <div className="search-section">
          <h1>Without Debouncing Demo</h1>
          <Form>
            <Form.Group controlId="search">
              <Form.Control
                type="search"
                placeholder="Enter text to search"
                onChange={this.handleInputChange}
                value={input}
                autoComplete="off"
              />
            </Form.Group>
            {errorMsg && <p>{errorMsg}</p>}
            {isLoading && <p className="loading">Loading...</p>}
            <ul className="search-result">
              {result.map((item, index) => (
                <li key={index}>{item.data.title}</li>
              ))}
            </ul>
          </Form>
        </div>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's a Code Sandbox Demo.

In the above code, we're displaying a search box where user types some value and we're calling the handleInputChange method on the onChange event of the input text box.

Inside that method, we're making an API call to reddit by passing the search string and we're storing the result in the results array in the state and displaying the result as an unordered list.

without_debouncing.gif

As you can see, on every character typed, we are making an API call. So we are unnecessarily increasing the server API calls.

If the server is taking more time to return the data, you might see the previous result even when you are expecting new results based on your input value.

To fix this, we can use debouncing where we only make an API request after half-second(500 milliseconds) once a user has stopped typing which is more beneficial. It will save from unnecessary requests and will also save from previous API call result being displayed for a short time.

With debouncing in class component

Here, we will use the debounce method provided by lodash library to add the debouncing functionality.

import React from 'react';
import axios from 'axios';
import _ from 'lodash';
import { Form } from 'react-bootstrap';

export default class WithDebouncingClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      input: '',
      result: [],
      errorMsg: '',
      isLoading: false
    };

    this.handleSearchText = _.debounce(this.onSearchText, 500);
  }

  onSearchText = (input) => {
    this.setState({ isLoading: true });

    axios
      .get(`https://www.reddit.com/search.json?q=${input}`)
      .then((result) => {
        this.setState({
          result: result.data.data.children,
          errorMsg: '',
          isLoading: false
        });
      })
      .catch(() => {
        this.setState({
          errorMsg: 'Something went wrong. Try again later.',
          isLoading: false
        });
      });
  };

  handleInputChange = (event) => {
    const input = event.target.value;
    this.setState({ input });
    this.handleSearchText(input);
  };

  render() {
    const { input, result, errorMsg, isLoading } = this.state;
    return (
      <div className="container">
        <div className="search-section">
          <h1>With Debouncing Demo</h1>
          <Form>
            <Form.Group controlId="search">
              <Form.Control
                type="search"
                placeholder="Enter text to search"
                onChange={this.handleInputChange}
                value={input}
                autoComplete="off"
              />
            </Form.Group>
            {errorMsg && <p>{errorMsg}</p>}
            {isLoading && <p className="loading">Loading...</p>}
            <ul className="search-result">
              {result.map((item, index) => (
                <li key={index}>{item.data.title}</li>
              ))}
            </ul>
          </Form>
        </div>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's a Code Sandbox Demo.

with_debouncing.gif

As you can see, with the added debouncing functionality, the API call is only made once after half-second(500 milliseconds) when we stopped typing, thereby reducing the number of API calls and also the result is not flickered and we're getting only the final result which is expected and useful behavior.

The lodash's debounce method accepts two parameters.

  • A function to execute
  • The number of milliseconds to wait before executing the passed function
this.handleSearchText = _.debounce(this.onSearchText, 500);
Enter fullscreen mode Exit fullscreen mode

The debounce method returns a function which we stored in this.handleSearchText class variable and we're calling it in handleInputChange handler which gets called when the user types something in the input search textbox.

When we call the handleSearchText method, it internally calls the onSearchText method where we're making an API call to reddit.

Note that, we're calling the debounce function inside the constructor because this initialization needs to be done only once.

Let's see how can we use debouncing when using React Hooks.

Without Debouncing in React hooks

Let's first write the code without debouncing using hooks.

import React, { useState } from 'react';
import axios from 'axios';
import { Form } from 'react-bootstrap';

const WithoutDebouncingHooks = () => {
  const [input, setInput] = useState('');
  const [result, setResult] = useState([]);
  const [errorMsg, setErrorMsg] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleInputChange = (event) => {
    const input = event.target.value;
    setInput(input);
    setIsLoading(true);
    axios
      .get(`https://www.reddit.com/search.json?q=${input}`)
      .then((result) => {
        setResult(result.data.data.children);
        setErrorMsg('');
        setIsLoading(false);
      })
      .catch(() => {
        setErrorMsg('Something went wrong. Try again later.');
        setIsLoading(false);
      });
  };

  return (
    <div className="container">
      <div className="search-section">
        <h1>Without Debouncing Demo</h1>
        <Form>
          <Form.Group controlId="search">
            <Form.Control
              type="search"
              placeholder="Enter text to search"
              onChange={handleInputChange}
              value={input}
              autoComplete="off"
            />
          </Form.Group>
          {errorMsg && <p>{errorMsg}</p>}
          {isLoading && <p className="loading">Loading...</p>}
          <ul className="search-result">
            {result.map((item, index) => (
              <li key={index}>{item.data.title}</li>
            ))}
          </ul>
        </Form>
      </div>
    </div>
  );
};

export default WithoutDebouncingHooks;
Enter fullscreen mode Exit fullscreen mode

Here's a Code Sandbox Demo.

without_debouncing_hooks.gif

This is the same code of debouncing without class written using hooks.

Let's see how we can add debouncing to this code.

With debouncing in React hooks

import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import _ from 'lodash';
import { Form } from 'react-bootstrap';

const WithDebouncingHooks = () => {
  const [input, setInput] = useState('');
  const [result, setResult] = useState([]);
  const [errorMsg, setErrorMsg] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const inputRef = useRef();

  useEffect(() => {
    // initialize debounce function to search once user has stopped typing every half second
    inputRef.current = _.debounce(onSearchText, 500);
  }, []);

  const onSearchText = (input) => {
    setIsLoading(true);
    axios
      .get(`https://www.reddit.com/search.json?q=${input}`)
      .then((result) => {
        setResult(result.data.data.children);
        setErrorMsg('');
        setIsLoading(false);
      })
      .catch(() => {
        setErrorMsg('Something went wrong. Try again later.');
        setIsLoading(false);
      });
  };

  const handleInputChange = (event) => {
    const input = event.target.value;
    setInput(input);
    inputRef.current(input);
  };

  return (
    <div className="container">
      <div className="search-section">
        <h1>With Debouncing Demo</h1>
        <Form>
          <Form.Group controlId="search">
            <Form.Control
              type="search"
              placeholder="Enter text to search"
              onChange={handleInputChange}
              value={input}
              autoComplete="off"
            />
          </Form.Group>
          {errorMsg && <p>{errorMsg}</p>}
          {isLoading && <p className="loading">Loading...</p>}
          <ul className="search-result">
            {result.map((item, index) => (
              <li key={index}>{item.data.title}</li>
            ))}
          </ul>
        </Form>
      </div>
    </div>
  );
};

export default WithDebouncingHooks;
Enter fullscreen mode Exit fullscreen mode

Here's a Code Sandbox Demo.

with_debouncing_hooks.gif

As you can see, only one API call is made when we use debouncing.

In the above code, we're calling the debounce function inside the useEffect hook by passing an empty array [] as a second argument because this code needs to be executed only once.

And we're storing the result of the function in inputRef.current. inputRef is a ref created by calling useRef() hook. It contains a current property which we can use to retain the value even after the component is re-rendered.

Using the local variable to store the result of debounce function will not work because for every re-render of the component previous variables will get lost. So React provided a ref way of persisting data across re-render inside the components using Hooks.

And then inside the handleInputChange handler, we're calling the function stored inside the inputRef.current variable.

const handleInputChange = (event) => {
 const input = event.target.value;
 setInput(input);
 inputRef.current(input);
};
Enter fullscreen mode Exit fullscreen mode

That's it about this article. I hope you enjoyed the article and found it useful.

You can find the complete source code for this application in this repository and live demo at this url

Don't forget to subscribe to get my weekly newsletter with amazing tips, tricks and articles directly in your inbox here.

Top comments (4)

Collapse
 
michaelphipps profile image
Phippsy • Edited

Random: Not a React coder at all, but a thought on how to Improve this further... after you call the api, as the user continues typing .filter() existing results. then you only need to call the api again if the user changes the initial query text or filter returns nothing and user continues typing (in case the original result was paged excluding the required result) If API returns 0, and user keeps typing without changing original query text you also dont need to hit the api again.

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Really Nice suggestion. Thanks 👍

Collapse
 
kunalt96 profile image
Kunal Tiwari

It's really brilliant, got it at the correct time, Have to implement same thing, Thanks a lot for this :)

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Glad it was helpful to you. Thank you 🙂