Lodash debounce в функциональном компоненте React не работает

У меня есть функциональный компонент, построенный на основе компонента Реагировать Таблица, который использует Клиент Apollo GraphQL для разбиения на страницы и поиска на стороне сервера. Я пытаюсь реализовать отмену дребезга для поиска, чтобы к серверу выполнялся только один запрос, как только пользователь перестанет вводить это значение. Я пробовал решения Лодаш и потрясающее обещание отказа, но все же запрос к серверу выполняется для каждого символа, введенного в поле поиска.

Вот мой компонент (с отредактированной неактуальной информацией):

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

... и вот результат: связь

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
41
0
24 381
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

debounceLoadData будет новая функция для каждого рендера. Вы можете использовать хук useCallback, чтобы убедиться, что одна и та же функция сохраняется между рендерингами, и она будет работать должным образом.

useCallback(debounce(loadData, 1000), []);

const { useState, useCallback } = React;
const { debounce } = _;

function App() {
  const [filter, setFilter] = useState("");
  const debounceLoadData = useCallback(debounce(console.info, 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>

При использовании этого шаблона я получаю предупреждение от eslint: «React Hook useCallback получил функцию, зависимости которой неизвестны. Вместо этого передайте встроенную функцию. eslintreact-hooks/exhaustive-deps». Любая идея, как я могу избежать этой ошибки? Я попытался обернуть debounce встроенной функцией, но затем debounce не смог выполнить свою работу.

user2602152 09.09.2020 11:06

@ user2602152 Я тоже получаю это предупреждение. Документация по хукам React предупреждает, что мемоизированные функции не гарантируют, что они не будут выгружены и повторно выполнены в будущем для освобождения памяти. Не уверен насчет хука useCallback.

Switch386 20.02.2021 21:12

Чтобы добавить к ответу Толле: если вы хотите в полной мере использовать хуки, вы можете использовать хук useEffect, чтобы следить за изменениями в фильтре и запускать функцию 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.info(filter);
  }

  return <input value = {filter} onChange = {event => setFilter(event.target.value)} />;
}

ReactDOM.render(<App />, document.getElementById("root"));

не зафиксирует ли он начальное значение filter внутри функции и никогда не обновит его?

Pavel Luzhetskiy 17.03.2020 13:04

@PavelLuzetskiy К сожалению, я сделал небольшую ошибку в своем решении, что позволяет мне понять, почему вы так думаете. Я отредактировал его сейчас, но сначала я делал debounce(console.info(filter), 1000), что не сработало, потому что передача console.info(filter) запустит его немедленно, а не вызовет. Передача filter => console.info(filter) в вызов debounce работает, и функция получит новое значение filter из вызова debounceLoadData(filter) в useEffect. В качестве альтернативы, передача функции без ее вызова (как в моем редактировании) также передает ей параметры.

Adriaan Marain 18.03.2020 14:19

это было полезно, спасибо, я использовал гораздо более сложную работу, чтобы добиться того же

Pavel Luzhetskiy 19.03.2020 10:04

Я думаю, что вам не нужно useEffect в этом случае, просто вызовите функцию debounced в обработчике onChange: return <input value = {filter} onChange = {handleChangeEvent} />;function handleChangeEvent(event) { setFilter(event.target.value); debounceLoadData(event.target.value) }

Peeke Kuepers 03.06.2020 11:12

Вы должны помнить об отключенной функции между рендерами.

Тем не менее, вы должны нет использовать useCallback, чтобы запомнить отклоненную (или дросселированную) функцию, как это предлагается в других ответах. useCallback предназначен для встроенных функций!

Вместо этого используйте useMemo, чтобы запомнить функцию debounce между рендерами:

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

Это также средство от ошибки React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead react-hooks/exhaustive-deps во времени CI.

vahdet 03.01.2021 20:57

Относительно 'useMemo'... reactjs.org/docs/hooks-reference.html#usememo Вы можете полагаться на useMemo как на оптимизацию производительности, а не как на семантическую гарантию. В будущем React может решить «забыть» некоторые ранее запомненные значения и пересчитать их при следующем рендеринге, например. освободить память для закадровых компонентов. Напишите свой код так, чтобы он по-прежнему работал без использования useMemo, а затем добавьте его для оптимизации производительности.

Switch386 20.02.2021 21:14

Другие вопросы по теме