React函数组件中的lodash防抖未能工作

52
我有一个基于React Table组件构建的功能组件,使用Apollo GraphQL客户端进行服务器端分页和搜索。我正在尝试为搜索实现防抖动,以便在用户停止键入该值时只执行一次对服务器的查询。我已尝试lodash debounceawesome debounce promise解决方案,但仍会针对搜索字段中键入的每个字符执行一次查询。
这是我的组件(已隐藏不相关信息):
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

...这是结果: 链接

4个回答

97

debounceLoadData将在每次渲染时生成一个新函数。您可以使用useCallback钩子确保相同的函数在渲染之间被保留并按预期工作。

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>


19
使用这个模式时,我从eslint得到一个警告:"React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-deps"。有什么方法可以避免这个错误吗?我尝试使用内联函数包装debounce,但是这样做会影响到debounce的功能。 - user2602152
@user2602152 我也收到了这个警告。React的hook文档警告说,记忆化函数不能保证它们不会在未来被卸载和重新执行以释放内存。不确定useCallback hook是否有同样的问题。 - Switch386
2
我也遇到了同样的问题,但是使用 useMemo 而不是 useCallback 解决了我的问题。 - Gabriel Brito

24

补充 Tholle 的回答:如果您想充分利用 hook,可以使用 useEffect hook 监听筛选器的变化,并在发生变化时运行 debouncedLoadData 函数:

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

1
它不会捕获函数内filter的初始值并永远不会更新吗? - Pavel Luzhetskiy
1
@PavelLuzhetskiy 哎呀,我在解决方案中犯了一个小错误,这让我明白了你为什么会这样想。我现在已经编辑过了,但是我最初执行的是 debounce(console.log(filter), 1000),它不起作用,因为传递 console.log(filter) 会立即运行它,而不是调用它。将 filter => console.log(filter) 传递给 debounce 调用可以正常工作,函数将从 useEffect 中的 debounceLoadData(filter) 调用接收到 filter 的新值。或者,传递一个没有调用的函数(就像我的编辑中一样)也会将参数传递给它。 - Adriaan Marain
2
那很有帮助,谢谢。我之前使用了更复杂的方法来实现同样的功能。 - Pavel Luzhetskiy
我认为在这种情况下你不需要使用 useEffect,只需在 onChange 处理程序中调用防抖函数即可:return <input value={filter} onChange={handleChange} />;function handleChange(event) { setFilter(event.target.value); debounceLoadData(event.target.value) } - Peeke Kuepers

16

在渲染之间,必须记住防抖函数。

但是,不应该像其他答案中建议的那样使用useCallback来记录防抖(或节流)函数。useCallback是为内联函数设计的!

相反,使用useMemo来记住在渲染之间的防抖函数:

useMemo(() => debounce(loadData, 1000), []);

2
它也是CI时间中“React Hook useCallback接收到一个依赖项未知的函数。请传递内联函数而不是react-hooks/exhaustive-deps”错误的解决方法。 - vahdet
6
有关'useMemo'的问题...https://reactjs.org/docs/hooks-reference.html#usememo您可以将useMemo用作性能优化,而不是语义保证。在未来,React可能会选择“忘记”一些先前记忆的值,并在下次渲染时重新计算它们,例如释放用于离屏组件的内存。编写代码时,请确保即使没有使用useMemo也可以正常工作,然后再添加它以优化性能。 - Switch386

0

希望这篇文章能帮助你找到解决方案,

你不必使用外部库来实现防抖,可以按照我的步骤创建自己的自定义钩子。

步骤(1):创建防抖的自定义钩子。

  
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
}

步骤(2):- 现在创建您想要添加节流的文件

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;

注意:- 在测试此文件之前,请先创建React应用程序,然后将我的文件放入其中

希望这个解决方案对您有所帮助


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接