Effectuer un anti-rebond dans React.js

497

Comment effectuez-vous un anti-rebond dans React.js?

Je veux faire rebondir le handleOnChange.

J'ai essayé avec debounce(this.handleOnChange, 200)mais ça ne marche pas.

function debounce(fn, delay) {
  var timer = null;
  return function() {
    var context = this,
      args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args);
    }, delay);
  };
}

var SearchBox = React.createClass({
  render: function() {
    return <input type="search" name="p" onChange={this.handleOnChange} />;
  },

  handleOnChange: function(event) {
    // make ajax call
  }
});
Chetan Ankola
la source
J'ai rencontré le même problème avec vous, de superbes réponses ci-dessous! mais je pense que vous avez mal utilisé debounce. ici, quand onChange={debounce(this.handleOnChange, 200)}/>, il invoquera à debounce functionchaque fois. mais, en fait, ce dont nous avons besoin est d'invoquer la fonction que la fonction anti-rebond a renvoyée.
pingfengafei

Réponses:

835

2019: essayez les hooks + promets debouncing

Ceci est la version la plus récente de la façon dont je résoudrais ce problème. J'utiliserais:

Il s'agit d'un câblage initial, mais vous composez vous-même des blocs primitifs et vous pouvez créer votre propre crochet personnalisé de sorte que vous n'ayez besoin de le faire qu'une seule fois.

// Generic reusable hook
const useDebouncedSearch = (searchFunction) => {

  // Handle the input text state
  const [inputText, setInputText] = useState('');

  // Debounce the original search async function
  const debouncedSearchFunction = useConstant(() =>
    AwesomeDebouncePromise(searchFunction, 300)
  );

  // The async callback is run each time the text changes,
  // but as the search function is debounced, it does not
  // fire a new request on each keystroke
  const searchResults = useAsync(
    async () => {
      if (inputText.length === 0) {
        return [];
      } else {
        return debouncedSearchFunction(inputText);
      }
    },
    [debouncedSearchFunction, inputText]
  );

  // Return everything needed for the hook consumer
  return {
    inputText,
    setInputText,
    searchResults,
  };
};

Et puis vous pouvez utiliser votre crochet:

const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text))

const SearchStarwarsHeroExample = () => {
  const { inputText, setInputText, searchResults } = useSearchStarwarsHero();
  return (
    <div>
      <input value={inputText} onChange={e => setInputText(e.target.value)} />
      <div>
        {searchResults.loading && <div>...</div>}
        {searchResults.error && <div>Error: {search.error.message}</div>}
        {searchResults.result && (
          <div>
            <div>Results: {search.result.length}</div>
            <ul>
              {searchResults.result.map(hero => (
                <li key={hero.name}>{hero.name}</li>
              ))}
            </ul>
          </div>
        )}
      </div>
    </div>
  );
};

Vous trouverez cet exemple en cours d'exécution ici et vous devriez lire la documentation react-async-hook pour plus de détails.


2018: essayez de promettre un rebond

Nous voulons souvent rebondir les appels d'API pour éviter d'inonder le backend de requêtes inutiles.

En 2018, travailler avec des rappels (Lodash / Underscore) me fait du mal et est sujet à erreur. Il est facile de rencontrer des problèmes de passe-partout et de concurrence en raison de la résolution d'appels d'API dans un ordre arbitraire.

J'ai créé une petite bibliothèque avec React en tête pour résoudre vos douleurs: awesome-debounce-promise .

Cela ne devrait pas être plus compliqué que cela:

const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text));

const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500);

class SearchInputAndResults extends React.Component {
  state = {
    text: '',
    results: null,
  };

  handleTextChange = async text => {
    this.setState({ text, results: null });
    const result = await searchAPIDebounced(text);
    this.setState({ result });
  };
}

La fonction anti-rebond garantit que:

  • Les appels d'API seront rejetés
  • la fonction anti-rebond renvoie toujours une promesse
  • seule la promesse retournée du dernier appel résoudra
  • un seul this.setState({ result });se produira par appel d'API

Finalement, vous pouvez ajouter une autre astuce si votre composant se démonte:

componentWillUnmount() {
  this.setState = () => {};
}

Notez que Observables (RxJS) peut également être un excellent choix pour les entrées anti-rebond, mais c'est une abstraction plus puissante qui peut être plus difficile à apprendre / à utiliser correctement.


<2017: vous voulez toujours utiliser la fonction anti-rebond?

La partie importante ici est de créer une seule fonction anti-rebond (ou limitée) par instance de composant . Vous ne voulez pas recréer la fonction anti-rebond (ou limitation) à chaque fois, et vous ne voulez pas que plusieurs instances partagent la même fonction anti-rebond.

Je ne définis pas de fonction anti-rebond dans cette réponse car elle n'est pas vraiment pertinente, mais cette réponse fonctionnera parfaitement avec le _.debouncesoulignement ou le lodash, ainsi qu'avec toute fonction anti-rebond fournie par l'utilisateur.


BONNE IDÉE:

Parce que les fonctions anti-rebond sont avec état, nous devons créer une fonction anti-rebond par instance de composant .

ES6 (propriété de classe) : recommandé

class SearchBox extends React.Component {
    method = debounce(() => { 
      ...
    });
}

ES6 (constructeur de classe)

class SearchBox extends React.Component {
    constructor(props) {
        super(props);
        this.method = debounce(this.method.bind(this),1000);
    }
    method() { ... }
}

ES5

var SearchBox = React.createClass({
    method: function() {...},
    componentWillMount: function() {
       this.method = debounce(this.method.bind(this),100);
    },
});

Voir JsFiddle : 3 instances produisent 1 entrée de journal par instance (ce qui en fait 3 globalement).


Pas une bonne idée:

var SearchBox = React.createClass({
  method: function() {...},
  debouncedMethod: debounce(this.method, 100);
});

Cela ne fonctionnera pas, car lors de la création de l'objet description de classe, ce thisn'est pas l'objet créé lui-même. this.methodne renvoie pas ce que vous attendez car le thiscontexte n'est pas l'objet lui-même (qui en réalité n'existe pas encore vraiment BTW car il vient d'être créé).


Pas une bonne idée:

var SearchBox = React.createClass({
  method: function() {...},
  debouncedMethod: function() {
      var debounced = debounce(this.method,100);
      debounced();
  },
});

Cette fois, vous créez effectivement une fonction anti-rebond qui appelle votre this.method. Le problème est que vous le recréez à chaque debouncedMethodappel, donc la fonction anti-rebond nouvellement créée ne sait rien des anciens appels! Vous devez réutiliser la même fonction anti-rebond au fil du temps, sinon le rebouncing ne se produira pas.


Pas une bonne idée:

var SearchBox = React.createClass({
  debouncedMethod: debounce(function () {...},100),
});

C'est un peu délicat ici.

Toutes les instances montées de la classe partageront la même fonction anti-rebond, et le plus souvent ce n'est pas ce que vous voulez!. Voir JsFiddle : 3 instances ne produisent qu'une seule entrée de journal dans le monde.

Vous devez créer une fonction anti-rebond pour chaque instance de composant , et non une seule fonction anti-rebond au niveau de la classe, partagée par chaque instance de composant.


Prenez soin de la mise en commun des événements de React

Ceci est lié au fait que nous voulons souvent rebondir ou limiter les événements DOM.

Dans React, les objets d'événement (c'est-à-dire SyntheticEvent) que vous recevez dans les rappels sont regroupés (cela est maintenant documenté ). Cela signifie qu'après l'appel du rappel d'événement, le SyntheticEvent que vous recevez sera remis dans le pool avec des attributs vides pour réduire la pression du GC.

Ainsi, si vous accédez aux SyntheticEventpropriétés de manière asynchrone par rapport au rappel d'origine (comme cela peut être le cas si vous accélérez / rebondissez), les propriétés auxquelles vous accédez peuvent être effacées. Si vous souhaitez que l'événement ne soit jamais remis dans le pool, vous pouvez utiliser la persist()méthode.

Sans persister (comportement par défaut: événement groupé)

onClick = e => {
  alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
  setTimeout(() => {
    alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
  }, 0);
};

Le 2ème (async) s'imprimera hasNativeEvent=falsecar les propriétés de l'événement ont été nettoyées.

Avec persister

onClick = e => {
  e.persist();
  alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
  setTimeout(() => {
    alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
  }, 0);
};

Le 2ème (async) s'imprimera hasNativeEvent=truecar persistvous permet d'éviter de remettre l'événement dans le pool.

Vous pouvez tester ces 2 comportements ici: JsFiddle

Lisez la réponse de Julen pour un exemple d'utilisation persist()avec une fonction de limitation / anti-rebond.

Sébastien Lorber
la source
3
Superbe réponse, c'est idéal pour définir l'état des champs de formulaire comme `` interagissant '' pendant quelques secondes après qu'ils arrêtent de taper, puis pouvoir annuler sur le formulaire de soumission ou surBlur
arush_try.com
8
Notez que dans ES6, au lieu de définir votre méthode à l'intérieur du constructeur (cela semble bizarre), vous pouvez le faire handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)au niveau supérieur de votre classe. Vous définissez toujours efficacement un membre d'instance, mais cela ressemble un peu plus à une définition de méthode normale. Pas besoin d'un constructorsi vous n'en avez pas déjà défini un. Je suppose que c'est surtout une préférence de style.
thom_nic
24
N'oubliez pas d'annuler la méthode anti-rebond dans componentWillUnmount: this.method.cancel()- sinon il peut vouloir définir setState sur un composant non monté.
elado
4
@JonasKello vous ne pouvez pas rebondir à l'intérieur d'un composant sans état car la fonction anti-rebond est en fait avec état. Vous avez besoin d'un composant avec état pour contenir cette fonction anti-rebond, mais vous pouvez appeler un composant sans état avec une fonction déjà anti-rebond si nécessaire.
Sebastien Lorber
2
Pourquoi toutes les réponses incluent _.debounce au lieu d'écrire la fonction? Il a besoin de toute la bibliothèque pour cette fonction?
chifliiiii
217

Composants non contrôlés

Vous pouvez utiliser la event.persist()méthode .

Un exemple suit en utilisant les traits de soulignement _.debounce():

var SearchBox = React.createClass({

  componentWillMount: function () {
     this.delayedCallback = _.debounce(function (event) {
       // `event.target` is accessible now
     }, 1000);
  },

  onChange: function (event) {
    event.persist();
    this.delayedCallback(event);
  },

  render: function () {
    return (
      <input type="search" onChange={this.onChange} />
    );
  }

});

Edit: Voir ce JSFiddle


Composants contrôlés

Mise à jour: l'exemple ci-dessus montre un composant non contrôlé . J'utilise des éléments contrôlés tout le temps, voici donc un autre exemple de ce qui précède, mais sans utiliser la event.persist()"ruse".

Un JSFiddle est également disponible . Exemple sans soulignement

var SearchBox = React.createClass({
    getInitialState: function () {
        return {
            query: this.props.query
        };
    },

    componentWillMount: function () {
       this.handleSearchDebounced = _.debounce(function () {
           this.props.handleSearch.apply(this, [this.state.query]);
       }, 500);
    },

    onChange: function (event) {
      this.setState({query: event.target.value});
      this.handleSearchDebounced();
    },

    render: function () {
      return (
        <input type="search"
               value={this.state.query}
               onChange={this.onChange} />
      );
    }
});


var Search = React.createClass({
    getInitialState: function () {
        return {
            result: this.props.query
        };
    },

    handleSearch: function (query) {
        this.setState({result: query});
    },

    render: function () {
      return (
        <div id="search">
          <SearchBox query={this.state.result}
                     handleSearch={this.handleSearch} />
          <p>You searched for: <strong>{this.state.result}</strong></p>
        </div>
      );
    }
});

React.render(<Search query="Initial query" />, document.body);

Edit: exemples mis à jour et JSFiddles pour React 0.12

Edit: exemples mis à jour pour résoudre le problème soulevé par Sébastien Lorber

Edit: mis à jour avec jsfiddle qui n'utilise pas de soulignement et utilise un anti-rebond javascript simple.

julen
la source
Cela ne fonctionne pas pour les entrées. La cible d'événement dans la fonction anti-rebond n'a plus de valeur ... donc l'entrée reste vide.
Etai
1
Légèrement complexe, cela. Vous devez faire un peu attention aux accessoires. Si vous le définissez, <input value={this.props.someprop}...il ne s'affichera pas correctement car la mise à jour à la pression de la touche ne revient pas dans le composant avant le rebond. Il est bon d'omettre le value=si vous êtes content que cela ne soit pas géré, mais si vous souhaitez pré-remplir la valeur et / ou la lier ailleurs, cela ne fonctionne évidemment pas.
Alastair Maw
1
@AlastairMaw la question avait un composant non contrôlé, c'est pourquoi la réponse l'a aussi. J'ai ajouté ci-dessous une version alternative pour les composants contrôlés, avec une valeur pré-remplie.
julen
2
cela est très dangereux si vous montez plusieurs fois le composant dans le DOM, voir stackoverflow.com/questions/23123138/…
Sebastien Lorber
4
Bien que ce soit une excellente réponse, je ne recommande pas d'utiliser en persistparticulier lorsqu'il peut y avoir beaucoup d'événements, comme le mousemove. J'ai vu le code devenir totalement insensible de cette façon. Il est beaucoup plus efficace d'extraire les données nécessaires de l'événement natif dans l'appel d'événement, puis d'appeler la fonction anti-rebond / limitée avec les données uniquement, PAS l'événement lui-même. Pas besoin de persister l'événement de cette façon
MrE
31

2019: utilisez le crochet réactif 'useCallback'

Après avoir essayé de nombreuses approches différentes, j'ai trouvé que l'utilisation useCallbackétait la plus simple et la plus efficace pour résoudre le problème des appels multiples de l'utilisation debouncedans un onChangeévénement.

Selon la documentation de l'API Hooks ,

useCallback renvoie une version mémorisée du rappel qui ne change que si l'une des dépendances a changé.

Le passage d'un tableau vide en tant que dépendance garantit que le rappel n'est appelé qu'une seule fois. Voici une implémentation simple:

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();
 };

J'espère que cela t'aides!

Sameer Ingavale
la source
3
Excellente solution si vous utilisez des crochets. Tu m'as sauvé encore plus d'heures de frustration. Merci!
Carl Edwards
Pourriez-vous s'il vous plaît expliquer pourquoi les appels multiples se produisent en premier lieu? Ne debounce()considère pas que le onChange()rappel est la même méthode de rappel?
El Anonimo
J'ai modifié cette solution pour qu'elle fonctionne dans mon application. J'ai d'abord dû déplacer la ligne const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);à l'intérieur du corps du composant de fonction ou React génère un message d'erreur sur l'utilisation du crochet en dehors de celui-ci. Ensuite , dans le onChangegestionnaire d'événements: <input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}.
El Anonimo
Voici comment j'ai utilisé cette solution pour permettre à l'utilisateur de taper une entrée, puis d'envoyer un appel API rebondi avec la valeur d'entrée une fois qu'il a fini de taper. stackoverflow.com/questions/59358092/… .
El Anonimo
14

J'ai trouvé cet article de Justin Tulk très utile. Après quelques tentatives, dans ce que l'on percevrait comme la manière la plus officielle avec react / redux, cela montre qu'il échoue en raison de la mise en commun synthétique des événements de React . Sa solution utilise ensuite un état interne pour suivre la valeur modifiée / entrée dans l'entrée, avec un rappel juste après setStatelequel appelle une action redux limitée / rebondie qui affiche des résultats en temps réel.

import React, {Component} from 'react'
import TextField from 'material-ui/TextField'
import { debounce } from 'lodash'

class TableSearch extends Component {

  constructor(props){
    super(props)

    this.state = {
        value: props.value
    }

    this.changeSearch = debounce(this.props.changeSearch, 250)
  }

  handleChange = (e) => {
    const val = e.target.value

    this.setState({ value: val }, () => {
      this.changeSearch(val)
    })
  }

  render() {

    return (
        <TextField
            className = {styles.field}
            onChange = {this.handleChange}
            value = {this.props.value}
        />
    )
  }
}
racémique
la source
14

Si tout ce dont vous avez besoin de l'objet événement est d'obtenir l'élément d'entrée DOM, la solution est beaucoup plus simple - il suffit de l'utiliser ref. Notez que cela nécessite Underscore :

class Item extends React.Component {
    constructor(props) {
        super(props);
        this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000);
    }
    saveTitle(){
        let val = this.inputTitle.value;
        // make the ajax call
    }
    render() {
        return <input 
                    ref={ el => this.inputTitle = el } 
                    type="text" 
                    defaultValue={this.props.title} 
                    onChange={this.saveTitle} />
    }
}
Yura
la source
2
defaultValue est ce que je veux! Merci beaucoup mach :)
Tazo leladze
14

Après avoir lutté avec les entrées de texte pendant un certain temps et n'ayant pas trouvé de solution parfaite par moi-même, j'ai trouvé cela sur npm: react-debounce-input .

Voici un exemple simple:

import React from 'react';
import ReactDOM from 'react-dom';
import {DebounceInput} from 'react-debounce-input';

class App extends React.Component {
state = {
    value: ''
};

render() {
    return (
    <div>
        <DebounceInput
        minLength={2}
        debounceTimeout={300}
        onChange={event => this.setState({value: event.target.value})} />

        <p>Value: {this.state.value}</p>
    </div>
    );
}
}

const appRoot = document.createElement('div');
document.body.appendChild(appRoot);
ReactDOM.render(<App />, appRoot);

Le composant DebounceInput accepte tous les accessoires que vous pouvez affecter à un élément d'entrée normal. Essayez-le sur codepen

J'espère que cela aide aussi quelqu'un d'autre et lui fait gagner du temps.

Hooman Askari
la source
Après avoir essayé de nombreuses solutions répertoriées ici, c'était certainement la plus simple.
Vadorequest
9

Avec, debouncevous devez conserver l'événement synthétique d'origine avec event.persist(). Voici un exemple de travail testé avec React 16+.

import React, { Component } from 'react';
import debounce from 'lodash/debounce'

class ItemType extends Component {

  evntHandler = debounce((e) => {
    console.log(e)
  }, 500);

  render() {
    return (
      <div className="form-field-wrap"
      onClick={e => {
        e.persist()
        this.evntHandler(e)
      }}>
        ...
      </div>
    );
  }
}
export default ItemType;

Avec le composant fonctionnel, vous pouvez le faire -

const Search = ({ getBooks, query }) => {

  const handleOnSubmit = (e) => {
    e.preventDefault();
  }
  const debouncedGetBooks = debounce(query => {
    getBooks(query);
  }, 700);

  const onInputChange = e => {
    debouncedGetBooks(e.target.value)
  }

  return (
    <div className="search-books">
      <Form className="search-books--form" onSubmit={handleOnSubmit}>
        <Form.Group controlId="formBasicEmail">
          <Form.Control type="text" onChange={onInputChange} placeholder="Harry Potter" />
          <Form.Text className="text-muted">
            Search the world's most comprehensive index of full-text books.
          </Form.Text>
        </Form.Group>
        <Button variant="primary" type="submit">
          Search
        </Button>
      </Form>
    </div>
  )
}

Références - - https://gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709 - https://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html

Mohan Dere
la source
1
fonctionne très bien la meilleure implémentation que j'ai trouvée jusqu'à présent
Vincent Tang
8

Si vous utilisez redux, vous pouvez le faire de manière très élégante avec un middleware. Vous pouvez définir un Debouncemiddleware comme:

var timeout;
export default store => next => action => {
  const { meta = {} } = action;
  if(meta.debounce){
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      next(action)
    }, meta.debounce)
  }else{
    next(action)
  }
}

Vous pouvez ensuite ajouter un anti-rebond aux créateurs d'actions, tels que:

export default debouncedAction = (payload) => ({
  type : 'DEBOUNCED_ACTION',
  payload : payload,
  meta : {debounce : 300}
}

Il existe en fait déjà un middleware que vous pouvez supprimer de npm pour le faire pour vous.

Mat
la source
je pense que ce middleware doit être le premier à être exécuté en applyMiddleware(...)chaîne si nous en avons plusieurs
Youssef
Le délai d'attente n'est pas initialisé et ce premier clearTimeout traitera non défini pour un paramètre. Pas bon.
Jason Rice
7

En utilisant ES6 CLASS et React 15.xx & lodash.debounce Im en utilisant les références de React ici car les pertes d'événement le lient en interne.

class UserInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userInput: ""
    };
    this.updateInput = _.debounce(this.updateInput, 500);
  }


  updateInput(userInput) {
    this.setState({
      userInput
    });
    //OrderActions.updateValue(userInput);//do some server stuff
  }


  render() {
    return ( <div>
      <p> User typed: {
        this.state.userInput
      } </p>
      <input ref = "userValue" onChange = {() => this.updateInput(this.refs.userValue.value) } type = "text" / >
      </div>
    );
  }
}

ReactDOM.render( <
  UserInput / > ,
  document.getElementById('root')
);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>


<div id="root"></div>

ACIER
la source
7

Beaucoup de bonnes informations ici déjà, mais pour être succinct. Cela fonctionne pour moi ...

import React, {Component} from 'react';
import _ from 'lodash';

class MyComponent extends Component{
      constructor(props){
        super(props);
        this.handleChange = _.debounce(this.handleChange.bind(this),700);
      }; 
chad steele
la source
Ça ne marche pas pour moi. L'état ne se met pas à jour. Si je retire l' _debounce emballage, cela fonctionne. J'aime bien cette idée!
Mote Zart
Je devrais voir votre code pour en offrir beaucoup ici, mais je soupçonne qu'il se passe autre chose ... j'espère que cette réponse beaucoup plus approfondie apportera un éclairage. stackoverflow.com/questions/23123138/…
chad steele
6

Vous pouvez utiliser la méthode https://lodash.com/docs/4.17.5#debounce de Lodash debounce . C'est simple et efficace.

import * as lodash from lodash;

const update = (input) => {
    // Update the input here.
    console.log(`Input ${input}`);     
}

const debounceHandleUpdate = lodash.debounce((input) => update(input), 200, {maxWait: 200});

doHandleChange() {
   debounceHandleUpdate(input);
}

Vous pouvez également annuler la méthode anti-rebond en utilisant la méthode ci-dessous.

this.debounceHandleUpdate.cancel();

J'espère que cela vous aide. À votre santé!!

Dinesh Madhanlal
la source
5

FYI

Voici une autre implémentation PoC:

  • sans aucune bibliothèque (par exemple lodash) pour anti-rebond
  • à l'aide de l'API React Hooks

J'espère que ça aide :)

import React, { useState, useEffect, ChangeEvent } from 'react';

export default function DebouncedSearchBox({
  inputType,
  handleSearch,
  placeholder,
  debounceInterval,
}: {
  inputType?: string;
  handleSearch: (q: string) => void;
  placeholder: string;
  debounceInterval: number;
}) {
  const [query, setQuery] = useState<string>('');
  const [timer, setTimer] = useState<NodeJS.Timer | undefined>();

  useEffect(() => {
    if (timer) {
      clearTimeout(timer);
    }
    setTimer(setTimeout(() => {
      handleSearch(query);
    }, debounceInterval));
  }, [query]);

  const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
    setQuery(e.target.value);
  };

  return (
    <input
      type={inputType || 'text'}
      className="form-control"
      placeholder={placeholder}
      value={query}
      onChange={handleOnChange}
    />
  );
}
kenju
la source
4

Il existe un use-debouncepackage que vous pouvez utiliser avec les crochets ReactJS.

À partir du fichier README du package:

import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000);

  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}

Comme vous pouvez le voir dans l'exemple ci-dessus, il est configuré pour mettre à jour la variable valueune seule fois par seconde (1000 millisecondes).

Art
la source
3

Juste une autre variante avec une réaction et un lodash récents.

class Filter extends Component {
  static propTypes = {
    text: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired
  }

  state = {
    initialText: '',
    text: ''
  }

  constructor (props) {
    super(props)

    this.setText = this.setText.bind(this)
    this.onChange = _.fp.debounce(500)(this.onChange.bind(this))
  }

  static getDerivedStateFromProps (nextProps, prevState) {
    const { text } = nextProps

    if (text !== prevState.initialText) {
      return { initialText: text, text }
    }

    return null
  }

  setText (text) {
    this.setState({ text })
    this.onChange(text)
  }

  onChange (text) {
    this.props.onChange(text)
  }

  render () {
    return (<input value={this.state.text} onChange={(event) => this.setText(event.target.value)} />)
  }
}
puchu
la source
3

Une solution agréable et propre, qui ne nécessite aucune dépendance externe:

Anti-rebond avec React Hooks

Il utilise un personnalisé plus les hooks useEffect React et la méthode setTimeout/ clearTimeout.

Bruno Silvano
la source
3

As-tu essayé?

function debounce(fn, delay) {
  var timer = null;
  return function() {
    var context = this,
      args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args);
    }, delay);
  };
}

var SearchBox = React.createClass({
  render: function() {
    return <input type="search" name="p" onChange={this.handleOnChange} />;
  },

  handleOnChange: function(event) {
    debounce(\\ Your handleChange code , 200);
  }
});
Jivko Jelev
la source
2

Au lieu d'envelopper le handleOnChange dans un debounce (), pourquoi ne pas envelopper l'appel ajax dans la fonction de rappel à l'intérieur du debounce, ne détruisant ainsi pas l'objet événement. Donc quelque chose comme ça:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}
Robert
la source
4
Parce que l'objet événement n'est pas immuable et est détruit par ReactJS, donc même si vous encapsulez et atteignez une capture de fermeture, le code échouera.
Henrik
2

Voici un exemple que j'ai trouvé qui encapsule une autre classe avec un debouncer. Cela se prête bien à être transformé en fonction de décorateur / d'ordre supérieur:

export class DebouncedThingy extends React.Component {
    static ToDebounce = ['someProp', 'someProp2'];
    constructor(props) {
        super(props);
        this.state = {};
    }
    // On prop maybe changed
    componentWillReceiveProps = (nextProps) => {
        this.debouncedSetState();
    };
    // Before initial render
    componentWillMount = () => {
        // Set state then debounce it from here on out (consider using _.throttle)
        this.debouncedSetState();
        this.debouncedSetState = _.debounce(this.debouncedSetState, 300);
    };
    debouncedSetState = () => {
        this.setState(_.pick(this.props, DebouncedThingy.ToDebounce));
    };
    render() {
        const restOfProps = _.omit(this.props, DebouncedThingy.ToDebounce);
        return <Thingy {...restOfProps} {...this.state} />
    }
}
mlucool
la source
2

Il existe désormais une autre solution pour React et React Native fin 2019 :

composant react-debounce

<input>
<Debounce ms={500}>
  <List/>
</Debounce>

C'est un composant, facile à utiliser, minuscule et pris en charge par widley

Exemple:

entrez la description de l'image ici

import React from 'react';
import Debounce from 'react-debounce-component';

class App extends React.Component {
  constructor (props) {
    super(props);
    this.state = {value: 'Hello'}
  }
  render () {
    return (
      <div>
        <input value={this.state.value} onChange={(event) => {this.setState({value: event.target.value})}}/>
        <Debounce ms={1000}>
          <div>{this.state.value}</div>
        </Debounce>
      </div>
    );
  }
}

export default App;

* Je suis le créateur de ce composant

Rebs
la source
1

Je cherchais une solution au même problème et suis tombé sur ce fil ainsi que sur d'autres, mais ils avaient le même problème: si vous essayez de faire une handleOnChangefonction et que vous avez besoin de la valeur d'une cible d'événement, vous obtiendrez cannot read property value of nullou certains une telle erreur. Dans mon cas, j'avais également besoin de préserver le contexte de l' thisintérieur de la fonction anti-rebond puisque j'exécute une action fluxable. Voici ma solution, cela fonctionne bien pour mon cas d'utilisation, donc je la laisse ici au cas où quelqu'un rencontrerait ce fil:

// at top of file:
var myAction = require('../actions/someAction');

// inside React.createClass({...});

handleOnChange: function (event) {
    var value = event.target.value;
    var doAction = _.curry(this.context.executeAction, 2);

    // only one parameter gets passed into the curried function,
    // so the function passed as the first parameter to _.curry()
    // will not be executed until the second parameter is passed
    // which happens in the next function that is wrapped in _.debounce()
    debouncedOnChange(doAction(myAction), value);
},

debouncedOnChange: _.debounce(function(action, value) {
    action(value);
}, 300)
Edward
la source
1

pour throttleou debouncela meilleure façon est de créer un créateur de fonction afin que vous puissiez l' utiliser tout où, par exemple:

  updateUserProfileField(fieldName) {
    const handler = throttle(value => {
      console.log(fieldName, value);
    }, 400);
    return evt => handler(evt.target.value.trim());
  }

et dans votre renderméthode, vous pouvez faire:

<input onChange={this.updateUserProfileField("givenName").bind(this)}/>

la updateUserProfileFieldméthode créera une fonction séparée à chaque appel.

Note ne pas essayer de retourner le gestionnaire directement par exemple cela ne fonctionnera pas:

 updateUserProfileField(fieldName) {
    return evt => throttle(value => {
      console.log(fieldName, value);
    }, 400)(evt.target.value.trim());
  }

la raison pour laquelle cela ne fonctionnera pas car cela générera une nouvelle fonction de limitation chaque fois que l'événement sera appelé au lieu d'utiliser la même fonction de limitation, de sorte que la limitation sera inutile;)

Aussi , si vous utilisez debounceou throttlevous n'avez pas besoin setTimeoutou clearTimeout, c'est en fait la raison pour laquelle nous les utilisons: P

Alnamrouti à prix réduit
la source
1

Voici un extrait utilisant l'approche d'Abra @ enveloppé dans un composant de fonction (nous utilisons le tissu pour l'interface utilisateur, il suffit de le remplacer par un simple bouton)

import React, { useCallback } from "react";
import { debounce } from "lodash";

import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';

const debounceTimeInMS = 2000;

export const PrimaryButtonDebounced = (props) => {

    const debouncedOnClick = debounce(props.onClick, debounceTimeInMS, { leading: true });

    const clickHandlerDebounced = useCallback((e, value) => {

        debouncedOnClick(e, value);

    },[]);

    const onClick = (e, value) => {

        clickHandlerDebounced(e, value);
    };

    return (
        <PrimaryButton {...props}
            onClick={onClick}
        />
    );
}
Thread Pitt
la source
1

Ma solution est basée sur les hooks (écrite en Typescript).

J'ai 2 crochets principaux useDebouncedValueetuseDebouncedCallback

Première - useDebouncedValue

Disons que nous avons une boîte de recherche, mais nous voulons demander au serveur les résultats de la recherche après que l'utilisateur a arrêté de taper pendant 0,5 s

function SearchInput() {
  const [realTimeValue, setRealTimeValue] = useState('');

  const debouncedValue = useDebouncedValue(realTimeValue, 500); // this value will pick real time value, but will change it's result only when it's seattled for 500ms

  useEffect(() => {
    // this effect will be called on seattled values
    api.fetchSearchResults(debouncedValue);
  }, [debouncedValue])

  return <input onChange={event => setRealTimeValue(event.target.value)} />
}

la mise en oeuvre

import { useState, useEffect } from "react";

export function useDebouncedValue<T>(input: T, time = 500) {
  const [debouncedValue, setDebouncedValue] = useState(input);

  // every time input value has changed - set interval before it's actually commited
  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(input);
    }, time);

    return () => {
      clearTimeout(timeout);
    };
  }, [input, time]);

  return debouncedValue;
}

Seconde useDebouncedCallback

Il crée simplement une fonction «anti-rebond» dans la portée de votre composant.

Disons que nous avons un composant avec un bouton qui affichera une alerte 500 ms après avoir cessé de cliquer dessus.

function AlertButton() {
  function showAlert() {
    alert('Clicking has seattled');
  }

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

  return <button onClick={debouncedShowAlert}>Click</button>
}

Implémentation (notez que j'utilise lodash / debounce comme aide)

import debounce from 'lodash/debounce';
import { useMemo } from 'react';

export function useDebouncedCallback<T extends (...args: any) => any>(callback: T, wait?: number) {
  const debouncedCallback = useMemo(() => debounce(callback, wait), [callback, wait]);

  return debouncedCallback;
}
pie6k
la source
0

Voici un exemple TypeScript fonctionnel pour ceux qui utilisent TS et qui souhaitent rebondir sur les asyncfonctions.

function debounce<T extends (...args: any[]) => any>(time: number, func: T): (...funcArgs: Parameters<T>) => Promise<ReturnType<T>> {
     let timeout: Timeout;

     return (...args: Parameters<T>): Promise<ReturnType<T>> => new Promise((resolve) => {
         clearTimeout(timeout);
         timeout = setTimeout(() => {
             resolve(func(...args));
         }, time)
     });
 }
Andrei
la source
0

un peu tard ici mais cela devrait aider. créer cette classe (son écrit en tapuscrit mais il est facile de le convertir en javascript)

export class debouncedMethod<T>{
  constructor(method:T, debounceTime:number){
    this._method = method;
    this._debounceTime = debounceTime;
  }
  private _method:T;
  private _timeout:number;
  private _debounceTime:number;
  public invoke:T = ((...args:any[])=>{
    this._timeout && window.clearTimeout(this._timeout);
    this._timeout = window.setTimeout(()=>{
      (this._method as any)(...args);
    },this._debounceTime);
  }) as any;
}

et utiliser

var foo = new debouncedMethod((name,age)=>{
 console.log(name,age);
},500);
foo.invoke("john",31);
anaval
la source
0

vous pouvez utiliser tlence tlence

function log(server) {
  console.log('connecting to', server);
}

const debounceLog = debounce(log, 5000);
// just run last call to 5s
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');
Behnam Mohammadi
la source
0

La solution de Julen est un peu difficile à lire, voici un code de réaction plus clair et précis pour quiconque l'a trébuché en fonction du titre et non des petits détails de la question.

tl; dr version : lorsque vous mettriez à jour les observateurs, envoyez un appel à une méthode de planification à la place et cela à son tour avertira les observateurs (ou effectuera ajax, etc.)

Jsfiddle complet avec exemple de composant jsfiddle

var InputField = React.createClass({

    getDefaultProps: function () {
        return {
            initialValue: '',
            onChange: null
        };
    },

    getInitialState: function () {
        return {
            value: this.props.initialValue
        };
    },

    render: function () {
        var state = this.state;
        return (
            <input type="text"
                   value={state.value}
                   onChange={this.onVolatileChange} />
        );
    },

    onVolatileChange: function (event) {
        this.setState({ 
            value: event.target.value 
        });

        this.scheduleChange();
    },

    scheduleChange: _.debounce(function () {
        this.onChange();
    }, 250),

    onChange: function () {
        var props = this.props;
        if (props.onChange != null) {
            props.onChange.call(this, this.state.value)
        }
    },

});
srcspider
la source
3
Cela ne rendra-t-il pas l'état / le timing du rebond global dans toutes les instances de InputField, car il est créé avec la définition de classe? C'est peut-être ce que vous voulez, mais cela vaut la peine d'être noté malgré tout.
vole
1
dangereux si monté plusieurs fois dans le dom, consultez stackoverflow.com/questions/23123138/…
Sebastien Lorber
2
C'est une mauvaise solution, en raison de problèmes de double montage - vous faites votre fonction pour planifierChange un singleton et ce n'est pas une bonne idée. -1
Henrik
0

Évitez d'utiliser event.persist()- vous voulez laisser React recycler l'événement synthétique. Je pense que le moyen le plus propre, que vous utilisiez des classes ou des hooks, est de diviser le rappel en deux parties:

  1. Le rappel sans rebond
  2. Appelle une fonction anti-rebond avec uniquement les éléments de l'événement dont vous avez besoin (afin que l'événement synthétique puisse être recyclé)

Des classes

handleMouseOver = throttle(target => {
  console.log(target);
}, 1000);

onMouseOver = e => {
  this.handleMouseOver(e.target);
};

<div onMouseOver={this.onMouseOver} />

Les fonctions

const handleMouseOver = useRef(throttle(target => {
  console.log(target);
}, 1000));

function onMouseOver(e) {
  handleMouseOver.current(e.target);
}

<div onMouseOver={this.onMouseOver} />

Notez que si votre handleMouseOverfonction utilise l' état de l' intérieur du composant, vous devez utiliser au useMemolieu de useRefet transmettre les dépendances que sinon vous allez travailler avec des données périmées (ne concerne pas les classes de cours).

Dominique
la source
0

Étendre le crochet useState

import { useState } from "react";
import _ from "underscore"
export const useDebouncedState = (initialState, durationInMs = 500) => {
    const [internalState, setInternalState] = useState(initialState);
    const debouncedFunction = _.debounce(setInternalState, durationInMs);
    return [internalState, debouncedFunction];
};
export default useDebouncedState;

Utiliser un crochet

import useDebouncedState from "../hooks/useDebouncedState"
//...
const [usernameFilter, setUsernameFilter] = useDebouncedState("")
//...
<input id="username" type="text" onChange={e => setUsernameFilter(e.target.value)}></input>

https://trippingoncode.com/react-debounce-hook/

Micah B.
la source
0

J'ai rencontré ce problème aujourd'hui. Résolu en utilisant setTimeoutet clearTimeout.

Je vais donner un exemple que vous pourriez adapter:

import React, { Component } from 'react'

const DEBOUNCE_TIME = 500

class PlacesAutocomplete extends Component {
  debounceTimer = null;

  onChangeHandler = (event) => {
    // Clear the last registered timer for the function
    clearTimeout(this.debounceTimer);

    // Set a new timer
    this.debounceTimer = setTimeout(
      // Bind the callback function to pass the current input value as arg
      this.getSuggestions.bind(null, event.target.value), 
      DEBOUNCE_TIME
    )
  }

  // The function that is being debounced
  getSuggestions = (searchTerm) => {
    console.log(searchTerm)
  }

  render() {
    return (
      <input type="text" onChange={this.onChangeHandler} />
    )
  }
}

export default PlacesAutocomplete

Vous pouvez également le refactoriser dans son propre composant de fonction:

import React from 'react'

function DebouncedInput({ debounceTime, callback}) {
  let debounceTimer = null
  return (
    <input type="text" onChange={(event) => {
      clearTimeout(debounceTimer);

      debounceTimer = setTimeout(
        callback.bind(null, event.target.value), 
        debounceTime
      )
    }} />
  )
}

export default DebouncedInput

Et utilisez-le comme:

import React, { Component } from 'react'
import DebouncedInput from '../DebouncedInput';

class PlacesAutocomplete extends Component {
  debounceTimer = null;

  getSuggestions = (searchTerm) => {
    console.log(searchTerm)
  }

  render() {
    return (
      <DebouncedInput debounceTime={500} callback={this.getSuggestions} />
    )
  }
}

export default PlacesAutocomplete
Francisco Hanna
la source