Enhancing Web App Responsiveness: The Power of Debouncing

Enhancing Web App Responsiveness: The Power of Debouncing

What is Debouncing?

Debouncing serves as a programming technique aimed at ensuring that a function isn't overly invoked, particularly in response to rapid or continuous events like scrolling, resizing, or typing. When applied to an event handler, debouncing postpones the execution of the handler until a specified period of inactivity has passed since the last triggering of the event.

For instance, consider a scenario where you're developing a dashboard displaying employee data fetched via an API. You're tasked with adding a search feature based on parameters, such as searching by first name. It's common to observe beginners in development implementing this feature incorrectly, as follows:

  1. They add an input field with an onChange event.

  2. Upon every change event, they immediately trigger an API request to fetch the latest data and update the table.

However, this approach is flawed. Here's the correct approach, which involves leveraging debouncing:

  1. Include an input field with an onChange event.

  2. Within the event handler, incorporate debouncing, ensuring that the API request is triggered after a brief delay, rather than immediately upon each change. This prevents unnecessary API calls, optimising performance and efficiency.

Why we need debouncing ?

Lets imagine you are not using debouncing in your application. This will effect -

  1. Excessive Network Requests - Without debouncing, actions such as search queries or autocomplete suggestions triggered by user input may result in multiple rapid requests to the server. This can overload the server and consume unnecessary bandwidth, leading to decreased performance and increased server load.

  2. UI Flickering - Rapid changes in user input without debouncing can cause UI elements to flicker or update rapidly, resulting in a jarring and unpleasant user experience. For example, an autocomplete dropdown may flicker as it rapidly updates with each keystroke.

  3. Inefficient Resource Usage: Continuous processing of rapid input changes without debouncing can consume unnecessary client-side processing resources, leading to decreased performance and increased battery usage, particularly on mobile devices.

  4. Delayed Responses: In some cases, failing to debounce user input can lead to delayed responses or actions as the application waits for input to stabilize before processing. This can make the application feel unresponsive and sluggish to users.

  5. Incorrect Data Processing: Without debouncing, user input events may be processed before the input has stabilized, leading to incorrect or incomplete data processing. For example, in a form validation scenario, rapid keystrokes may result in incomplete validation checks being performed.

  6. Increased Server Load: In scenarios where user input triggers server-side processing, such as live search functionality, failing to debounce can result in unnecessary server load as each keystroke generates a separate request, potentially impacting server performance and scalability.

How to implement Debouncing in React?

Let's explore how to integrate debouncing into a React.js application. We'll utilize a JSON server mock API for this demonstration. To begin, let's initialize an empty React app with TypeScript.

npx create-react-app concept-debouncing --template typescript

it will create a new directory with name ```concept-debouncing```. open that directory in code editor.

create a new component in component directory called ListWrapper.tsx and add following code snippet

import { useEffect, useState } from 'react'

type Employee = {
  id: string;
  name: string;
}
const ListWrapper: React.FC = () => {
  const [employees, setEmployees] = useState<Employee[]>([])
  return (
    <div className="employee-wrapper">
      <h1>Search Employee</h1>
      <div className='search-box'>
        <input type="text" onChange={(e) => console.log(e)} className="search-text" placeholder='Search By Name' />
        <button type="button" className='search-btn'> Search </button>
      </div>
      {employees.map((employee, index) =>
      (
        <div className="employee-details" key={index}>
          <p>{employee.name}</p>
        </div>
      )
      )}
    </div>
  );
}

export default ListWrapper

use this ListWrapper in App.tsx

import './App.css';
import ListWrapper from './Components/ListWrapper';

function App() {
  return (
    <ListWrapper/>
  );
}

export default App;

Add following css to your App.css file

@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');

:root {
  --theme-background-color: #5895ff;

  --theme-button-color: #5895ff;

  --color-light: #fff;
}

* {
  font-family: 'Poppins', sans-serif;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  user-select: none;
}

body {
  background: var(--theme-background-color);
  display: flex;
  justify-content: center;
  align-items: center;
}

h1 {
  color: var(--color-light);
  margin-bottom: 0.5rem;
  font-size: 1.75rem;
}
.employee-wrapper {
  background: #1A1A40;
  margin-top: 5rem;
  padding: 2rem;
  border-radius: 5px;
  text-align: center;
}

.search-box {
  width: 100%;
}

.search-text {
  outline: none;
  background: none;
  border: 1px solid var(--theme-background-color);
  padding: 0.5rem 1rem;
  margin-top: 1rem;
  margin-bottom: 2rem;
  width: 300px;
  color: var(--color-light);
}

.search-text::placeholder {
  color: #ffffff4d;
}
.search-btn {
  background: var(--theme-button-color);
  color: var(--color-light);
  border: none;
  padding: 0.55rem;
  cursor: pointer;

}

.employee-details {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: var(--theme-background-color);
  color: var(--color-light);
  padding: 0.75rem 1rem;
  border-radius: 5px;
  margin-bottom: 1rem;
  cursor: pointer;
}

now run ``npm run start``. It will open the application at localhost:3000

you will see below screen

now let start API integration part. Open ListWrapper.tsx file and integrate the API. use can use axios if you want. integrate the API in the useEffect hook

  useEffect(() => {
    getUsersList()
  }, [])

  const getUsersList = () => {
    axios.get("https://jsonplaceholder.typicode.com/users?_limit=5").then((response: AxiosResponse) => {
      const employeeList = response.data
      setEmployees(employeeList)
    }).catch((error: AxiosError) => {
      //handle error accordingly
    })
  }

now you will see that your list is populated. Now we will come to the search part

for that we have take a new state in the component itself for searching purpose.

 const handleSearch = (event) => {
    setSearchParam(event.target.value)
  }

and call this event handler in the input field

<div className="employee-wrapper">
      <h1>Search Employee</h1>
      <div className='search-box'>
        <input type="text" onChange={(e) => handleSearch(e)} className="search-text" placeholder='Search By Name' />
        <button type="button" className='search-btn'> Search </button>
      </div>
      {employees.map((employee, index) =>
      (
        <div className="employee-details" key={index}>
          <p>{employee.name}</p>
        </div>
      )
      )}
    </div>

now we will add debouncing using useEffect hook

   useEffect(() => {
    let debounceTimeout: NodeJS.Timeout = null;
    //clear the previous timeout
    clearTimeout(debounceTimeout)
    // create new timeout with latest search param
    debounceTimeout = setTimeout(() => {
      getUsersList()
    }, 1500)

    return () => {
      clearTimeout(debounceTimeout)
    }
  }, [searchParam])

so in this way you can implement debouncing in react js.

Final code for ListWrapper.tsx file

import axios, { AxiosError, AxiosResponse } from 'axios';
import { useEffect, useState } from 'react'

type Employee = {
  id: string;
  name: string;
}
const ListWrapper: React.FC = () => {
  const [employees, setEmployees] = useState<Employee[]>([])
  const [searchParam, setSearchParam] = useState('')

  useEffect(() => {
    let debounceTimeout: NodeJS.Timeout = null;

    clearTimeout(debounceTimeout)

    debounceTimeout = setTimeout(() => {
      getUsersList()
    }, 1500)

    return () => {
      clearTimeout(debounceTimeout)
    }
  }, [searchParam])

  useEffect(() => {
    getUsersList()
  }, [])

  const getUsersList = () => {
    let apiURL = `https://jsonplaceholder.typicode.com/users?_limit=5`
    if(searchParam.length) {
      apiURL = apiURL + `&name=${searchParam}`
    }
    axios.get(apiURL).then((response: AxiosResponse) => {
      const employeeList = response.data
      setEmployees(employeeList)
    }).catch((error: AxiosError) => {
      //handle error accordingly
    })
  }

  const handleSearch = (event) => {
    setSearchParam(event.target.value)
  }

  return (
    <div className="employee-wrapper">
      <h1>Search Employee</h1>
      <div className='search-box'>
        <input type="text" onChange={(e) => handleSearch(e)} className="search-text" placeholder='Search By Name' />
        <button type="button" className='search-btn'> Search </button>
      </div>
      {employees.map((employee, index) =>
      (
        <div className="employee-details" key={index}>
          <p>{employee.name}</p>
        </div>
      )
      )}
      {
        !employees.length && <div className="employee-details">
        <p>No Record Found</p>
      </div>
      }
    </div>
  );
}

export default ListWrapper

Click here for Source code.