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
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>