50

I have a functional component built around the React Table component that uses the Apollo GraphQL client for server-side pagination and searching. I am trying to implement debouncing for the searching so that only one query is executed against the server once the user stops typing with that value. I have tried the lodash debounce and awesome debounce promise solutions but still a query gets executed against the server for every character typed in the search field.

Here is my component (with irrelevant info redacted):

import React, {useEffect, useState} from 'react';
import ReactTable from "react-table";
import _ from 'lodash';
import classnames from 'classnames';
import "react-table/react-table.css";
import PaginationComponent from "./PaginationComponent";
import LoadingComponent from "./LoadingComponent";
import {Button, Icon} from "../../elements";
import PropTypes from 'prop-types';
import Card from "../card/Card";
import './data-table.css';

import debounce from 'lodash/debounce';

function DataTable(props) {
    const [searchText, setSearchText] = useState('');
     const [showSearchBar, setShowSearchBar] = useState(false);

    const handleFilterChange = (e) => {
        let searchText = e.target.value;
        setSearchText(searchText);
        if (searchText) {
            debounceLoadData({
                columns: searchableColumns,
                value: searchText
            });
        }
    };

    const loadData = (filter) => {
        // grab one extra record to see if we need a 'next' button
        const limit = pageSize + 1;
        const offset = pageSize * page;

        if (props.loadData) {
            props.loadData({
                variables: {
                    hideLoader: true,
                    opts: {
                        offset,
                        limit,
                        orderBy,
                        filter,
                        includeCnt: props.totalCnt > 0
                    }
                },
                updateQuery: (prev, {fetchMoreResult}) => {
                    if (!fetchMoreResult) return prev;
                    return Object.assign({}, prev, {
                        [props.propName]: [...fetchMoreResult[props.propName]]
                    });
                }
            }).catch(function (error) {
                console.error(error);
            })
        }
    };

    const debounceLoadData = debounce((filter) => {
        loadData(filter);
    }, 1000);

    return (
        <div>
            <Card style={{
                border: props.noCardBorder ? 'none' : ''
            }}>
                {showSearchBar ? (
                        <span className="card-header-icon"><Icon className='magnify'/></span>
                        <input
                            autoFocus={true}
                            type="text"
                            className="form-control"
                            onChange={handleFilterChange}
                            value={searchText}
                        />
                        <a href="javascript:void(0)"><Icon className='close' clickable
                                                           onClick={() => {
                                                               setShowSearchBar(false);
                                                               setSearchText('');
                                                           }}/></a>
                ) : (
                        <div>
                           {visibleData.length > 0 && (
                                <li className="icon-action"><a 
href="javascript:void(0)"><Icon className='magnify' onClick= {() => {
    setShowSearchBar(true);
    setSearchText('');
}}/></a>
                                </li>
                            )}
                        </div>
                    )
                )}
                <Card.Body className='flush'>
                    <ReactTable
                        columns={columns}
                        data={visibleData}
                    />
                </Card.Body>
            </Card>
        </div>
    );
}

export default DataTable

... and this is the outcome: link

jrkt
  • 2,615
  • 5
  • 28
  • 48

4 Answers4

95

debounceLoadData will be a new function for every render. You can use the useCallback hook to make sure that the same function is being persisted between renders and it will work as expected.

useCallback(debounce(loadData, 1000), []);

const { useState, useCallback } = React;
const { debounce } = _;

function App() {
  const [filter, setFilter] = useState("");
  const debounceLoadData = useCallback(debounce(console.log, 1000), []);

  function handleFilterChange(event) {
    const { value } = event.target;

    setFilter(value);
    debounceLoadData(value);
  }

  return <input value={filter} onChange={handleFilterChange} />;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Tholle
  • 108,070
  • 19
  • 198
  • 189
  • 19
    When using this pattern, I get a warning from eslint: "React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-deps". Any idea to how I can avoid this error? I tried wrapping debounce with an inline function, but then debounce is not able to do it's job. – user2602152 Sep 09 '20 at 09:06
  • @user2602152 I get this warning as well. React's hook docs warn that memoized functions make no guarantee that they will not be offloaded and re-executed in the future to free up memory. Not sure about the useCallback hook. – Switch386 Feb 20 '21 at 20:12
  • 2
    I also had the same issue but using `useMemo` instead of `useCallback` solved my issue – Gabriel Brito May 11 '22 at 19:33
21

To add onto Tholle's answer: if you want to make full use of hooks, you can use the useEffect hook to watch for changes in the filter and run the debouncedLoadData function when that happens:

const { useState, useCallback, useEffect } = React;
const { debounce } = _;

function App() {
  const [filter, setFilter] = useState("");
  const debounceLoadData = useCallback(debounce(fetchData, 1000), []);

  useEffect(() => {
    debounceLoadData(filter);
  }, [filter]);

  function fetchData(filter) {
    console.log(filter);
  }

  return <input value={filter} onChange={event => setFilter(event.target.value)} />;
}

ReactDOM.render(<App />, document.getElementById("root"));
Adriaan Marain
  • 294
  • 3
  • 10
  • would it not capture the initial value of the `filter` inside the function and never update it? – Pavel Luzhetskiy Mar 17 '20 at 12:04
  • 1
    @PavelLuzhetskiy Whoops, I made a small mistake in my solution which makes me understand why you would think that. I have edited it now, but I was first doing `debounce(console.log(filter), 1000)`, which didn't work because passing `console.log(filter)` will run it immediately, instead of calling it. Passing `filter => console.log(filter)` to the `debounce` call does work, and the function will receive the new value of `filter` from the `debounceLoadData(filter)` call in `useEffect`. Alternatively, passing a function without calling it (like in my edit) will also pass it the parameters. – Adriaan Marain Mar 18 '20 at 13:19
  • 2
    that was helpful thanks, I was using much more complex work around to achieve the same – Pavel Luzhetskiy Mar 19 '20 at 09:04
  • I think you don't need `useEffect` in this case, just call the debounced function in the onChange handler: `return ;` `function handleChange(event) { setFilter(event.target.value); debounceLoadData(event.target.value) }` – Peeke Kuepers Jun 03 '20 at 09:12
16

You must remember the debounced function between renders.

However, you should not use useCallback to remember a debounced (or throttled) function as suggested in other answers. useCallback is designed for inline functions!

Instead use useMemo to remember the debounced function between renders:

useMemo(() => debounce(loadData, 1000), []);
Stuck
  • 11,225
  • 11
  • 59
  • 104
  • 2
    It is also a remedy for `React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead react-hooks/exhaustive-deps` error in CI-time – vahdet Jan 03 '21 at 19:57
  • 6
    Regarding 'useMemo'...https://reactjs.org/docs/hooks-reference.html#usememo You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance. – Switch386 Feb 20 '21 at 20:14
0

I hope this post will get you to the solution ,

You don't have to use external library for Debouncing you can create your own custom hook follow my steps

step(1):- Create the custom hook of Debouncing

  
import { useEffect ,useState} from 'react';


export const  UseDebounce = (value,delay)=>{

  const [debouncedValue,setDebouncedValue]= useState();

useEffect(()=>{
let timer = setTimeout(()=>setDebouncedValue(value),delay)


return ()=> clearTimeout(timer);

},[value])

return debouncedValue
}

step(2) :- Now create the file in which you want to add throttle

import React from 'react'
import { useEffect } from 'react';
import { useState } from 'react';
import {UseDebounce} from "./UseDebounce";



function Test() {
    const [input, setInput] = useState("");
     const debouncedValue = UseDebounce(input,1000);
  
    
    const handleChange = (e)=>{
        setInput(e.target.value)
    }

    useEffect(()=>{
        UseDebounce&& console.log("UseDebounce",UseDebounce)
    },[UseDebounce])
    


  return (
    <div>
    <input type="text" onChange={handleChange} value={input}/>
    {UseDebounce}
    </div>
  )
}

export default Test;

NOTE:- To test this file first create react app then embrace my files in it

Hope this solution worthwhile to you