web-dev-qa-db-fra.com

Effectuer un rebounce dans React.js

Comment effectuez-vous debounce 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
    }
});
355
Chetan Ankola

En 2018: essayez de vous débarrasser de vos promesses

Nous voulons souvent rejeter les appels d'API pour éviter d'inonder le back-end de requêtes inutiles.

En 2018, le fait de travailler avec des callbacks (Lodash/Underscore) me semble pénible et source d'erreurs. Il est facile de rencontrer des problèmes standard et de concurrence dus à la résolution des appels d'API dans un ordre arbitraire.

J'ai créé une petite bibliothèque avec React en tête pour résoudre vos problèmes: 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 debounce assure que:

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

Finalement, vous pouvez ajouter un autre truc si votre composant se démonte:

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

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


Vous voulez toujours utiliser le rappel anti-rappel?

La partie importante ici est pour créer une seule fonction décomposée (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 ce n’est pas vraiment pertinent, mais cette réponse fonctionnera parfaitement avec _.debounce de trait de soulignement ou de lodash, ainsi que toute fonction anti-rebond fournie par l’utilisateur.


BONNE IDÉE:

Comme les fonctions debounce ont un état, nous devons créer one fonction 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,1000);
    }
    method() { ... }
}

ES5

var SearchBox = React.createClass({
    method: function() {...},
    componentWillMount: function() {
       this.method = debounce(this.method,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 d'un objet de description de classe, this n'est pas l'objet créé lui-même. this.method ne renvoie pas ce que vous attendez car le contexte this n'est pas l'objet lui-même (qui n'existe pas encore réellement 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-échec qui appelle votre this.method. Le problème est que vous le recréez à chaque appel debouncedMethod, de sorte que la fonction anti-rebond nouvellement créée ne sait rien des appels antérieurs! Vous devez réutiliser la même fonction anti-rebond au fil du temps, sans quoi le rebond n'aura pas lieu.


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 dérivée, 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 au niveau mondial.

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


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

Cela est lié au fait que nous souhaitons souvent atténuer 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 (c'est maintenant documenté ). Cela signifie qu'après l'appel du rappel d'événement, l'événement 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 propriétés SyntheticEvent de manière asynchrone par rapport au rappel d'origine (comme cela peut être le cas si vous étouffez/réduisez), les propriétés auxquelles vous accédez sont susceptibles d'être effacées. Si vous souhaitez que l'événement ne soit jamais remis dans le pool, vous pouvez utiliser la méthode persist().

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

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

Le 2nd (asynchrone) imprimera hasNativeEvent=false car les propriétés de l'événement ont été nettoyées.

Avec persist

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

2nd (async) imprimera hasNativeEvent=true car persist vous permet d’éviter de replacer l’événement dans le pool.

Vous pouvez tester ces 2 comportements ici: JsFiddle

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

702
Sebastien Lorber

Composants non contrôlés

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

Un exemple suit en utilisant la fonction __ underscore _.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, mais sans utiliser la "ruse" event.persist().

Un JSFiddle est disponible aussi. Exemple sans trait de 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 to 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 trait de soulignement et utilise un rebounce javascript pur.

192
julen

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: utilisez simplement ref. Notez que cela nécessite Souligner :

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} />
    }
}
11
Yura

J'ai trouvé ce post par Justin Tulk très utile. Après quelques tentatives, de la manière la plus officielle avec réaction/redux, cela montre qu’il échoue à cause de la mise en commun des événements synthétiques 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 setState qui appelle une action redux étranglée/annulée qui affiche certains 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}
        />
    )
  }
}
8
lxm7

Après avoir lutté pendant un certain temps avec les entrées de texte sans trouver la solution parfaite, j'ai trouvé ceci sur npm https://www.npmjs.com/package/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 aidera aussi quelqu'un d'autre et lui fera gagner du temps.

6
Hooman Askari

Utilisation de ES6 CLASS et React 15.x.x & lodash.debounceIm en utilisant les réactions de refs here de React puisque les pertes d’événements sont liées 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>

6
STEEL

Beaucoup de bonnes informations ici déjà, mais pour être bref. 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);
      }; 
5
chad steele

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

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’action, tels que:

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

Il existe en fait déjà un middleware vous pouvez utiliser npm pour le faire à votre place.

5
Matt

Vous pouvez utiliser Lodash debounce https://lodash.com/docs/4.17.5#debounce method. 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 ça vous aide. À votre santé!!

4
Dinesh Madhanlal

Voici un exemple que j'ai créé et qui enveloppe une autre classe avec un debouncer. Cela se prête bien à une 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} />
    }
}
2
mlucool

Au lieu d'envelopper le handleOnChange dans une debounce (), pourquoi ne pas envelopper l'appel ajax dans la fonction de rappel à l'intérieur de l'anti-rebond, sans détruire l'objet événement Donc, quelque chose comme ça:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}
1
Robert

Je cherchais une solution au même problème et je suis tombé sur ce fil ainsi que sur d'autres, mais le problème est le même: si vous essayez d'effectuer une fonction handleOnChange et que vous avez besoin de la valeur d'une cible d'événement, vous obtiendrez cannot read property value of null ou une telle erreur. Dans mon cas, je devais aussi préserver le contexte de this dans la fonction debounce puisque j'exécute une action fluxible. Voici ma solution, cela fonctionne bien pour mon cas d'utilisation, donc je le laisse ici au cas où quelqu'un tomberait sur ce fil de discussion:

// 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)
1
Edward

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

rebours avec React crochets

Il utilise une combinaison plus les crochets useEffect React et la méthode setTimeout/clearTimeout.

1
Bruno Silvano

Avec debounce, vous 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;

Référence Gist - https://Gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709

1
Mohan Dere

pour throttle ou debounce, le meilleur moyen est de créer un créateur de fonction afin de pouvoir l'utiliser n'importe où, par exemple:

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

et dans votre méthode render, vous pouvez faire:

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

la méthode updateUserProfileField créera une fonction séparée chaque fois que vous l'appelez.

Remarque n'essayez pas de renvoyer 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 d’accélération à chaque fois que l’événement appelé appellera au lieu d’utiliser la même fonction d’accélérateur;

De même, si vous utilisez debounce ou throttle, vous n'avez pas besoin de setTimeout ou clearTimeout, c'est pourquoi nous les utilisons: P

1
Fareed Alnamrouti

FYI

Voici une autre implémentation PoC:

  • sans librairies (par exemple, lodash) pour le rebond
  • utilisation de l'API React Hooks

J'espère que ça aide :)

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';

function debounced(func, wait) {
  let timeout;
  return (...args) => {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func(args);
    }, wait);
  };
}

export default function DebouncedSearchBox(props) {
  const [query, setQuery] = useState('');

  useEffect(() => {
    debounced(() => {
      props.handleSearch(query);
    }, props.debounceInterval)();
  })

  const handleOnChange = (evt) => {
    setQuery(evt.target.value);
  }

  return (
    <input
      type="text"
      placeholder="name or email"
      value={query}
      onChange={handleOnChange}
    />
  );
}

DebouncedSearchBox.propTypes = {
  handleSearch: PropTypes.func.isRequired,
  debounceInterval: PropTypes.number.isRequired,
}
1
phi

Voici un exemple de travail TypeScript pour ceux qui utilisent TS et qui souhaitent éviter les fonctions async.

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)
     });
 }
0
Andrei

un peu tard ici mais cela devrait aider. créer cette classe (écrite en TypeScript mais facile à 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 d'utiliser

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

vous pouvez utiliser tlence https://www.npmjs.com/package/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');
0
Behnam Mohammadi

Juste une autre variante avec les réactions récentes et lodash.

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)} />)
  }
}
0
puchu

La solution de Julen est un peu difficile à lire, voici un code plus clair et qui réagit directement à celui qui l’a trébuché en se basant sur le titre et non sur les petits détails de la question.

tl; dr version: lorsque vous souhaitez mettre à jour des observateurs, envoyez à call une méthode de planification et, à son tour, avertira les observateurs (ou exécutera ajax, etc.)

Terminez jsfiddle avec l’exemple de composant http://jsfiddle.net 7/7655p5L/4/

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)
        }
    },

});
0
srcspider