lodash debounce in React functional component not working

24,272

Solution 1

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>

Solution 2

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"));

Solution 3

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), []);
Share:
24,272
jrkt
Author by

jrkt

Updated on July 09, 2022

Comments

  • jrkt
    jrkt almost 2 years

    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

  • Pavel Luzhetskiy
    Pavel Luzhetskiy about 4 years
    would it not capture the initial value of the filter inside the function and never update it?
  • Adriaan Marain
    Adriaan Marain about 4 years
    @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.
  • Pavel Luzhetskiy
    Pavel Luzhetskiy about 4 years
    that was helpful thanks, I was using much more complex work around to achieve the same
  • Peeke Kuepers
    Peeke Kuepers almost 4 years
    I think you don't need useEffect in this case, just call the debounced function in the onChange handler: return <input value={filter} onChange={handleChange} />; function handleChange(event) { setFilter(event.target.value); debounceLoadData(event.target.value) }
  • user2602152
    user2602152 over 3 years
    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.
  • vahdet
    vahdet over 3 years
    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
  • Switch386
    Switch386 about 3 years
    @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
    Switch386 about 3 years
    Regarding 'useMemo'...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.
  • Gabriel Brito
    Gabriel Brito almost 2 years
    I also had the same issue but using useMemo instead of useCallback solved my issue