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
Debouncing enforces that there is a minimum time gap between two consecutive invocations of a function call. For example, a debounce interval of 500ms means that if 500ms hasn't passed from the previous invocation attempt, we cancel the previous invocation and schedule the next invocation of the function after 500ms.
Here's a simple implementation : import React, { useCallback } from "react"; import { debounce } from "lodash"; const handler = useCallback(debounce(someFunction, 2000), []); const onChange = (event) => { // perform any event related action here handler(); };
Debouncing is a programming practice used to ensure that time-consuming tasks do not fire so often, that it stalls the performance of the web page. In other words, it limits the rate at which a function gets invoked.
useEffect() is for side-effects. A functional React component uses props and/or state to calculate the output. If the functional component makes calculations that don't target the output value, then these calculations are named side-effects.
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>
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"));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With