lodash debounce in React functional component not working
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), []);
jrkt
Updated on July 09, 2022Comments
-
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 about 4 yearswould it not capture the initial value of the
filter
inside the function and never update it? -
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 passingconsole.log(filter)
will run it immediately, instead of calling it. Passingfilter => console.log(filter)
to thedebounce
call does work, and the function will receive the new value offilter
from thedebounceLoadData(filter)
call inuseEffect
. Alternatively, passing a function without calling it (like in my edit) will also pass it the parameters. -
Pavel Luzhetskiy about 4 yearsthat was helpful thanks, I was using much more complex work around to achieve the same
-
Peeke Kuepers almost 4 yearsI 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 over 3 yearsWhen 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 over 3 yearsIt 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 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 about 3 yearsRegarding '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 almost 2 yearsI also had the same issue but using
useMemo
instead ofuseCallback
solved my issue