VOOZH about

URL: https://blog.logrocket.com/create-custom-debounce-hook-react/

โ‡ฑ Create a custom debounce Hook in React - LogRocket Blog


2022-11-16
1667
#react
Ishan Manandhar
141230
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

React v16.8 introduced React Hooks and a new wave of possibilities for writing functional components. With React Hooks, we can create reusable logic separately, which helps us write better and more manageable code.

๐Ÿ‘ Create a Custom Debounce Hook in React

In this article, we will write a custom debounce Hook in our React app that defers some number of seconds to process our passed value. We will create a simple React app to search for Rick and Morty characters and optimize our application with a custom debounce React Hook.

Jump ahead:

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

What is debouncing?

Debouncing is an optimizing technique for the result of any expensive function. These functions can take a lot of execution effort, so we will create this effect by simulating a delayed execution for a period. Using debounce can improve our React application by chunking the executed actions, adding a delayed response, and executing the last call.

There are a lot of built-in Hooks in React, including use, useState, useEffect, useMemo, useContext, to name a few. We can even combine multiple Hooks and create custom Hooks for our React application. Custom React Hooks are functions that start with use keywords followed by the name of the Hook we are making.

Before creating a debounce Hook, we need to understand the following:

  • Hooks are not called inside a loop, conditions, or nested functions
  • Multiple Hooks can be used to build new ones
  • Hooks can only be called from React functions
  • Hooks are made for functional components.
  • Name Hooks starting with the word โ€œuseโ€

We are building an application that will simulate excessive API calls to the backend sent by pressing keystrokes. To reduce the excessive calls, we will introduce the useDebounce React Hook. This Hook will consume the value and the timer we passed.

We will only execute the most recent user action if the action is continuously invoked. Using the debounce React Hook will reduce the unnecessary API calls to our server and optimize our application for better performance.

Letโ€™s get started!

Creating our React app

For this application, we will be using a simple React, Vite.js, and JavaScript application. We will add a few sprinkles of styling with Chakra UI.

Letโ€™s create a Vite.js application with this command:

npm create vite@latest react-useDebounce
cd react-useDebounce
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

After all the packages have been added, we can start creating the components folder. We will begin with a simple TextField that we will import from Chakra:

export default function Inputfield({onChange, value}) {
 return (
 <div>
 <Input onChange={onChange} value={value} placeholder='Search your character' size='md' />
 </div>
 )
}

This component is a simple input that takes in an onChange function and value as a prop. We will use a response from our API to list all the characters we find.

We need to call our endpoint and receive the response from it, so we will create a Utils folder and get data using the browser native fetch API:

export async function getCharacter(value) {
 const data = await fetch(
 `https://rickandmortyapi.com/api/character/?name=${value}`
 )
 const response = await data.json()
 if (response === undefined || response.error) {
 throw new Error(`HTTP error! status: ${response.error}`);
 }
 return response
}

Here, we created a function that makes an API call to the server and parses the data to JSON. We also added a basic error handling in case we receive an error or undefined from the server.

Writing our custom debounce React Hook

Now, we can go ahead and create a Hooks folder where we will add the Hooks we create for our application. You can brush up on the best practices for using React Hooks here.

Inside of useDebounce.jsx, we will write our useDebounce function:

import { useState, useEffect } from 'react'

export const useDebounce = (value, milliSeconds) => {
 const [debouncedValue, setDebouncedValue] = useState(value);

 useEffect(() => {
 const handler = setTimeout(() => {
 setDebouncedValue(value);
 }, milliSeconds);

 return () => {
 clearTimeout(handler);
 };
 }, [value, milliSeconds]);

 return debouncedValue;
};

You can see nothing much going on in this function, but donโ€™t worry, we will fix this as we go along. This is nothing new if you are familiar with the setTimeOut and clearTimeOut functions.

The function takes value and milliseconds as a second parameter, extending its execution with a specific time interval. We also cleared the time with a cleanup return call and added the value and milliSeconds as a dependency array. Hereโ€™s some more information about the functions:

  • useState(): This Hook helps us store the needed values
  • useEffect(): Used to update the debounce value with a cleanup function
  • setTimeOut(): Creates timeout delays
  • clearTimeOut: Clean up, dismounting the component relating to user input

We can implement our debounce React Hook inside our application:

import { useState, useEffect } from 'react'
import { ChakraProvider, Heading, Text, Box } from '@chakra-ui/react'
import Inputfield from './components/input-field'
import { useDebounce } from './hooks/useDebounce'
import { getCharacter } from './utils/getCharacter'
import './App.css'

function App() {
 const [query, setQuery] = useState('')
 const [listing, setListing] = useState('')
 const [loading, setLoading] = useState(false)

 const searchQuery = useDebounce(query, 2000)

 useEffect(() => {
 setListing('')
 if (searchQuery || query.length < 0) searchCharacter();
 async function searchCharacter() {
 setListing('')
 setLoading(true)
 const data = await getCharacter(searchQuery)
 setListing(data.results)
 setLoading(false)
 }
 }, [searchQuery])

 return (
 <div className="App">
 <ChakraProvider>
 <Heading mb={4}>Search Rick and Morty Character</Heading>
 <Text fontSize='md' textAlign="left" mb={10}>
 With a debouce hook implemented
 </Text>
 <Inputfield mb={10} onChange={(event) => setQuery(event.target.value)} value={query} />
 {loading && <Text mb={10} mt={10} textAlign="left">Loading...</Text>}
 {listing && <Box mt={10} display={'block'}>{listing.map(character => (
 <Box key={character.id} mb={10}>
 <img src={character.image} alt={character.name} />
 {character.name}
 </Box>
 ))}</Box>}
 </ChakraProvider>
 </div>
 )
}

export default App

So far, weโ€™ve done the basic implementation and used useState to store state for our searchQuery word.

After finding the result, weโ€™ll set our listing state with the data. Because this is an asynchronous action, we added loading to continue tracking the data loading state.

Although this is a simple implementation of a debounce Hook, we will improve and refactor our code. Letโ€™s get into improving our code.

Improving our debounce Hook in React

To improve the debounce Hook in React, we will use AbortController, a WebAPI natively built-in with all modern browsers. This API helps us stop any ongoing Web requests.

To start using this controller, instantiate it with the following:

const controller = new AbortController();


Over 200k developers use LogRocket to create better digital experiences

๐Ÿ‘ Image
Learn more โ†’

With the controller, we can access two properties:

  • abort() : When executed, this cancels the ongoing request
  • Signal: This maintains the connection between the controller and requests to cancel

We can now add further tweaks to our debounce Hook. When we do not receive a milliSeconds value, weโ€™ll provide an optional value:

const timer = setTimeout(() => setDebouncedValue(value), milliSeconds || 1000)

Inside the getCharacter function, we will pass in the signal property of the controller. Now, we will make some significant changes to our main file.

Letโ€™s go through the changes that were introduced:

import { useState, useEffect, useRef } from 'react'
import { ChakraProvider, Heading, Text, Box, Button, SimpleGrid } from '@chakra-ui/react'
import Inputfield from './components/input-field'
import { useDebounce } from './hooks/useDebounce'
import { getCharacter } from './utils/getCharacter'
import './App.css'

function App() {
 const [query, setQuery] = useState('')
 const [listing, setListing] = useState('')
 const [loading, setLoading] = useState(false)
 const controllerRef = useRef()

 const searchQuery = useDebounce(query, 2000)
 const controller = new AbortController();
 controllerRef.current = controller;
 const searchCharacter = async () => {
 setListing('')
 setLoading(true)
 const data = await getCharacter(searchQuery, controllerRef.current?.signal)
 controllerRef.current = null;
 setListing(data.results)
 setLoading(false)
 }

 useEffect(() => {
 if (searchQuery || query.trim().length < 0) searchCharacter()
 return cancelSearch()
 }, [searchQuery])

 const cancelSearch = () => {
 controllerRef.current.abort();
 }

 return (
 <div className="App">
 <ChakraProvider>
 <Heading mb={4}>Search Rick and Morty Character</Heading>
 <Text fontSize='md' textAlign="left" mb={10}>
 With a debounce hook implemented
 </Text>

 <SimpleGrid columns={1} spacing={10}>
 <Box>
 <Inputfield mb={10} onChange={(event) => setQuery(event.target.value)} value={query} />
 </Box>
 </SimpleGrid>

 {loading && <Text mb={10} mt={10} textAlign="left">Loading...</Text>}
 {listing && <Box mt={10} display={'block'}>{listing.map(character => (
 <Box key={character.id} mb={10}>
 <img src={character.image} alt={character.name} />
 {character.name}
 </Box>
 ))}</Box>}
 {!listing && !loading && <Box mt={10} display={'block'} color={'#c8c8c8'}>You have started your search</Box>}
 </ChakraProvider>
 </div>
 )
}

export default App

Here, we introduced an additional Hook into our app. We used the controller constructor to create a new instance of AbortSignal and assigned the controller to useRef. The useRef helped us get the elements from the DOM to keep an eye on the state changes.



During our API call, we passed in the current signal option with controllerRef.current.signal. We added a cancel controller to call in the cleanup function when the searchQuery values changed:

  • Aborted: A Boolean value that indicates the signal has been aborted, itโ€™s initially false, and when fired, it is originally null
  • abortController.abort(): This helps us stop the fetch request

We can also make multiple calls to the server and abort the request as needed. This comes in handy when dealing with network traffic and optimization techniques.

Conclusion

In this article, we successfully created a debounce React Hook to limit unnecessary calls and processing to the server in our React application. Using this technique helps improve React applications.

We can use this debounce optimization technique for expensive actions like resizing events, dragging events, keystroke listeners, and on scroll events. This can help us run applications for known performance benefits. To find the complete working code, check out the GitHub repository.

Happy coding!

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

๐Ÿ‘ Image
Emmanuel John
Jun 17, 2026 โ‹… 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

๐Ÿ‘ Image
Chizaram Ken
Jun 16, 2026 โ‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ€” no new framework required.

๐Ÿ‘ Image
Ikeh Akinyemi
Jun 12, 2026 โ‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now