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
}
});
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:
this.setState({ result });
se produira par appel d'APIFinalement, 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.
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.
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).
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éé).
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.
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.
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()
.
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.
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.
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
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.
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} />
}
}
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}
/>
)
}
}
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.
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>
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);
};
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.
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é!!
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} />
}
}
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);
}
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)
Une solution agréable et propre, ne nécessitant aucune dépendance externe:
Il utilise une combinaison plus les crochets useEffect React et la méthode setTimeout
/clearTimeout
.
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
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
FYI
Voici une autre implémentation PoC:
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,
}
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)
});
}
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);
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');
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)} />)
}
}
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)
}
},
});