28
votes

Le rebond de lodash dans le composant fonctionnel React ne fonctionne pas

J'ai un composant fonctionnel construit autour du composant React Table qui utilise le client Apollo GraphQL pour la pagination et la recherche côté serveur. J'essaye d'implémenter le debouncing pour la recherche afin qu'une seule requête soit exécutée contre le serveur une fois que l'utilisateur arrête de taper avec cette valeur. J'ai essayé les solutions anti-rebond de lodash et les solutions de promesse anti-rebond impressionnantes , mais une requête est toujours exécutée contre le serveur pour chaque caractère saisi dans le champ de recherche.

Voici mon composant (avec des informations non pertinentes expurgées):

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 & (
                                
  • { setShowSearchBar(true); setSearchText(''); }}/></a> </li> )} </div> ) )} <Card.Body className='flush'> <ReactTable columns={columns} data={visibleData} /> </Card.Body> </Card> </div> ); } export default DataTable
  • ... et voici le résultat: lien


    0 commentaires

    3 Réponses :


    66
    votes

    debounceLoadData sera une nouvelle fonction pour chaque rendu. Vous pouvez utiliser le hook useCallback pour vous assurer que la même fonction est persistante entre les rendus et qu'elle fonctionnera comme prévu.

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

    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"));
    useCallback(debounce(loadData, 1000), []);
    


    2 commentaires

    Lors de l'utilisation de ce modèle, j'obtiens un avertissement de eslint: "React Hook useCallback a reçu une fonction dont les dépendances sont inconnues. Passez plutôt une fonction en ligne. Eslintreact-hooks / exhaust-deps". Une idée de la façon dont je peux éviter cette erreur? J'ai essayé d'envelopper le debounce avec une fonction en ligne, mais le debounce n'est pas capable de faire son travail.


    @ user2602152 remplace useCallback(debounce(...)) par useMemo(() => debounce(...)) . Voyez ma réponse.



    8
    votes

    Pour ajouter à la réponse de Tholle: si vous souhaitez utiliser pleinement les hooks, vous pouvez utiliser le hook useEffect pour surveiller les changements dans le filtre et exécuter la fonction debouncedLoadData lorsque cela se produit:

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


    4 commentaires

    ne capturerait-il pas la valeur initiale du filter à l'intérieur de la fonction et ne le mettrait-il jamais à jour?


    @PavelLuzhetskiy Oups, j'ai fait une petite erreur dans ma solution qui me fait comprendre pourquoi vous penseriez cela. Je l'ai édité maintenant, mais je faisais d'abord debounce(console.log(filter), 1000) , ce qui n'a pas fonctionné parce que passer console.log(filter) lancera immédiatement, au lieu de l'appeler. Passer filter => console.log(filter) à l'appel filter => console.log(filter) - debounce fonctionne, et la fonction recevra la nouvelle valeur de filter de l'appel useEffect debounceLoadData(filter) dans useEffect . Alternativement, passer une fonction sans l'appeler (comme dans mon édition) lui passera également les paramètres.


    cela a été utile merci, j'utilisais un travail beaucoup plus complexe pour atteindre le même


    Je pense que vous n'avez pas besoin de useEffect dans ce cas, appelez simplement la fonction debounce dans le gestionnaire onChange: return <input value={filter} onChange={handleChange} />; function handleChange(event) { setFilter(event.target.value); debounceLoadData(event.target.value) }



    0
    votes

    Vous devez vous souvenir de la fonction rebondie entre les rendus.

    Cependant, vous ne devez pas utiliser useCallback pour vous souvenir d'une fonction débondrée (ou limitée) comme suggéré dans d'autres réponses. useCallback est conçu pour les fonctions en ligne!

    À la place, utilisez useMemo pour vous souvenir de la fonction useMemo -rebond entre les rendus:

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


    0 commentaires