1

used debounce function implementation without lodash from the gist credited below

  1. utils.js
export const debounce = (fn, delay) => {
  let timer = null;
  return function (...args) {
      const context = this;
      timer && clearTimeout(timer);
      timer = setTimeout(() => {
          fn.apply(context, args);
      }, delay);
  };
}
  1. Header
import React, { useEffect, useRef, useState, useContext } from 'react';
import SearchContext from "../Search/context"
import { debounce } from "../../utils";

function Header() {
const [searchBox, setSearchBox] = useState(false)
const ref = useRef(null)
const inputRef = useRef(null)
const { searchInput, updateSearchInput } = useContext(SearchContext)
...
render(
...
<input className="searchInput"
 ref={inputRef}
 value={searchInput}
 onChange={(e) => debounce(updateSearchInput(e.currentTarget.value), 2000)}
 onBlur={() => setSearchBox(false)}
 type="text" maxLength="80" />
...
)

}
export default Header;
  1. Search
import React, { useContext, useRef, useEffect, useState, useCallback } from "react"
import { debounce } from "../../utils";
import searchContext from "../Search/context"
import { apiCallMethod} from "../../api"

const Search = () => {
   const context = useContext(searchContext)
   const [results, setResults] = useState(null)

// context.searchInput is the value of the input box provided by SearchContext.Provider

const fetchData = useCallback(async () => {
   // the input string is the query param for the api request  
   return await apiCallMethod(context.searchInput, null).then(response => {
   setResults(response.results)            

   })

}, [context]);

useEffect(() => {

   debounce(fetchData(), 2000)

   return () => setResults(null)
}, [fetchData])

render(...)
}
export default Search

A few things are happening in the components above

Since my application is fairly large, I am not directly making the API call from the html input onChange event. I have a react context hook setup as searchContext that stores the text value entered in the input box

the same string is provided to the Search component through the context provider and it is called context.searchInput

In Search, there is a useEffect() that calls the Api method which returns the response data for the render.

However, the debounce is not working as expected. its not returning the correct results sometimes because

  1. the result set does not represent the most recent input string, and
  2. chrome devtools shows that it is firing excessive requests for substrings before debounce

I have tried to wrap debounce in onChange in the Header component, in the Search component, and both at the same time. I have also tried to increase the delay from 500 to 2000 but it does not help.

I can also drop the useCallback hook if there is a sideeffect. Appreciate the feedback

references

debounce implementation 1 https://gist.github.com/simonw/c29de00c20fde731243cbac8568a3d7f

debounce implementation 2 https://gist.github.com/sachinKumarGautam/6f6ce23fb70eec5d03e16b504b63ae2d

Ridhwaan Shakeel
  • 981
  • 1
  • 20
  • 39
  • I don't want to get into details with your question, however, I've had a problem with debounce too, and this maybe can help https://stackoverflow.com/questions/65457979/why-my-debounce-functionality-is-not-working-in-a-react-app-with-hooks . The implementation in the answer works as expected. – Noob Jun 17 '21 at 00:58

2 Answers2

3

I find that using debounce directly from React is always tricky and leads to subtle bugs.

I prefer using a useDebounce hook, which solves this. Here's a draft to a solution:

const Search = () => {
   const context = useContext(searchContext)
   const [results, setResults] = useState(null)

   const input = context.searchInput;
   const dInput = useDebounce(input, 1000);

   const fetchData = useCallback(async () => {
     // the input string is the query param for the api request  
     return await apiCallMethod(dInput, null).then(response => {
       setResults(response.results)            

     })
   }, [dInput]);

  useEffect(() => {

     fetchData()

     return () => setResults(null)
  }, [dInput])
}

N G
  • 146
  • 1
  • i made the changes and it works nearly perfectly however it is making two requests, one for the first letter and the other for the full text. for example if i type 'garden', the two requests are `search?q=g` and `search?q=garden`. I'm using https://usehooks.com/useDebounce/. investigating – Ridhwaan Shakeel Jun 17 '21 at 15:41
0

The code should be like the below. Because your debounce function is expecting fn & delay as argument, so you need to pass the function updateSearchInput and delay. If you write updateSearchInput(e.currentTarget.value), you are calling the function and the function is executing there only. Also your debounce is returning another function as closure (timer) which is expecting ...args and which is passed to fn.apply, hence it should be e.currentTarget.value

//add a new local state for the input
const [searchText, setSearchText] = useState('')

//set the new local state to input
<input className="searchInput"
 ref={inputRef}
 value={searchText}
 onChange={(e) => setSearchText(e.target.value)}
 onBlur={() => setSearchBox(false)}
 type="text" maxLength="80" />
...

//add an useEffect to trigger debounce
useEffect(() => {
 debounce(updateSearchInput, 2000)(searchText);

}, [searchText]);


Vivek Bani
  • 3,703
  • 1
  • 9
  • 18
  • i made the changes but now the `input` box is only rendering one letter at a time. for example, if I type 'red', only 'd' is showing in the `input`, and three api calls are made `search?q=r`, `search?q=e`, `search?q=d`. the `value` and `onChange` are tied to the react context hook `const { searchInput, updateSearchInput } = useContext(SearchContext)` – Ridhwaan Shakeel Jun 17 '21 at 01:25