Execute o debounce em React.js

Como você executa o debounce no React.js?

Eu quero debounce o handleOnChange.

Eu tentei com debounce(this.handleOnChange, 200) mas não funciona.

 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 (  ); }, handleOnChange: function (event) { //make ajax call } }); 

A parte importante aqui é criar uma única function debounced (ou regulada) por instância do componente . Você não quer recriar a function debounce (ou aceleração) toda vez, e você não quer que múltiplas instâncias compartilhem a mesma function debounced.

Eu não estou definindo a function debouncing nesta resposta, pois ela não é realmente relevante, mas esta resposta funcionará perfeitamente bem com _.debounce de sublinhado ou lodash, assim como a function de debouncing fornecida pelo usuário.


Não é uma boa ideia:

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

Não funcionará, porque durante a criação do object de descrição de class, this não é o object criado em si. this.method não retorna o que você espera porque o contexto this não é o object em si (o que realmente não existe ainda por enquanto está sendo criado).


Não é uma boa ideia:

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

Desta vez você está efetivamente criando uma function this.method que chama seu this.method . O problema é que você está recriando-o em todas as chamadas debouncedMethod , então a function de debounce recém-criada não sabe nada sobre chamadas antigas! Você deve reutilizar a mesma function debitada ao longo do tempo ou a ação de debitar não acontecerá.


Não é uma boa ideia:

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

Isso é um pouco complicado aqui.

Todas as instâncias montadas da class compartilharão a mesma function debitada e, na maioria das vezes, isso não é o que você deseja. Veja JsFiddle : 3 instâncias estão produzindo apenas 1 input de log globalmente.

Você tem que criar uma function debounced para cada instância do componente , e não uma function debanda do singe no nível da class, compartilhada por cada instância do componente.


BOA IDEIA:

Como as funções debounced são stateful, precisamos criar uma function debounced por instância do componente .

ES6 (propriedade da class) : recomendado

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

ES6 (construtor de class)

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

Veja JsFiddle : 3 instâncias estão produzindo 1 input de log por instância (que faz 3 globalmente).


Cuide do pool de events do React

Isso está relacionado porque muitas vezes queremos debounce ou estrangular events DOM.

No React, os objects de evento (ou seja, SyntheticEvent ) que você recebe em retornos de chamada são agrupados (isso é documentado agora). Isso significa que depois que o retorno de chamada do evento tiver sido chamado, o SyntheticEvent recebido será colocado de volta no conjunto com atributos vazios para reduzir a pressão do GC.

Portanto, se você acessar as propriedades SyntheticEvent como asynchronous ao retorno de chamada original (como pode ser o caso se você acelerar / debitar), as propriedades que você acessar poderão ser apagadas. Se você quiser que o evento nunca seja colocado de volta no pool, você pode usar o método persist() .

Sem persistência (comportamento padrão: evento agrupado)

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

O segundo (asynchronous) imprime hasNativeEvent=false porque as propriedades do evento foram limpas.

Com persistir

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

O segundo (asynchronous) imprime hasNativeEvent=true porque persist () permite evitar o retorno do evento no conjunto.

Você pode testar esses dois comportamentos aqui JsFiddle

Leia a resposta de Julen para um exemplo de uso de persist() com uma function de aceleração / debounce.

Componentes não controlados

Você pode usar o método event.persist() .

Um exemplo segue usando _.debounce() do sublinhado:

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

Editar: Veja este JSFiddle


Componentes controlados

Atualização: o exemplo acima mostra um componente não controlado . Eu uso elementos controlados o tempo todo, então aqui está outro exemplo do acima, mas sem usar o “truque” event.persist() .

Um JSFiddle também está disponível . Exemplo sem sublinhado

 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 (  ); } }); var Search = React.createClass({ getInitialState: function () { return { result: this.props.query }; }, handleSearch: function (query) { this.setState({result: query}); }, render: function () { return (  ); } }); React.render(, document.body); 

Edit: exemplos atualizados e JSFiddles para Reagir 0.12

Edit: exemplos atualizados para resolver o problema levantado por Sebastien Lorber

Edit: atualizado com jsfiddle que não usa sublinhado e usa debounce javascript simples.

Se tudo que você precisa do object event é obter o elemento de input DOM, a solução é muito mais simples – basta usar ref

 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  this.inputTitle = el } type="text" defaultValue={this.props.title} onChange={this.saveTitle} /> } } 

Eu encontrei este post por Justin Tulk muito útil. Depois de algumas tentativas, no que se percebe ser a maneira mais oficial com react / redux, isso mostra que ele falha devido ao agrupamento de events sintéticos do React . Sua solução então usa algum estado interno para rastrear o valor alterado / inserido na input, com um retorno de chamada logo após setState que chama uma ação redux regulada / debitada que mostra alguns resultados em tempo real.

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

Depois de lutar com as inputs de texto por um tempo e não encontrar uma solução perfeita sozinha, achei isso em npm https://www.npmjs.com/package/react-debounce-input

Aqui está um exemplo simples:

 import React from 'react'; import ReactDOM from 'react-dom'; import {DebounceInput} from 'react-debounce-input'; class App extends React.Component { state = { value: '' }; render() { return ( 
this.setState({value: event.target.value})} />

Value: {this.state.value}

); } } const appRoot = document.createElement('div'); document.body.appendChild(appRoot); ReactDOM.render(, appRoot);

O componente DebounceInput aceita todos os suportes que você pode atribuir a um elemento de input normal. Experimente em codepen

Espero que ajude alguém e economize tempo.

Usando ES6 CLASS e React 15.xx & lodash.debounce Estou usando os refs do React aqui, já que as perdas de evento estão ligadas internamente.

 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 ( 

User typed: { this.state.userInput }

this.updateInput(this.refs.userValue.value) } type = "text" / >
); } } ReactDOM.render( < UserInput / > , document.getElementById('root') );
    

Se você estiver usando redux, você pode fazer isso de uma maneira muito elegante com o middleware. Você pode definir um middleware de Debounce como:

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

Você pode então adicionar a ação de debounce aos criadores de ações, como:

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

Na verdade, já existe um middleware que você pode fazer com o npm para fazer isso por você.

Você pode usar o método debounce https://lodash.com/docs/4.17.5#debounce do Lodash. É simples e efetivo.

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

Você também pode cancelar o método de debounce usando o método abaixo.

 this.debounceHandleUpdate.cancel(); 

Espero que ajude você. Felicidades!!

Aqui está um exemplo que trouxe outra class com um debounce. Isso se presta muito bem a ser transformado em uma function de decorador / ordem superior:

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

Eu estava procurando uma solução para o mesmo problema e me deparei com este segmento, bem como alguns outros, mas eles tinham o mesmo problema: se você está tentando fazer uma function handleOnChange e você precisa do valor de um destino de evento, você receberá cannot read property value of null ou algum erro desse tipo. No meu caso, eu também precisava preservar o contexto this dentro da function debounce, já que estou executando uma ação fluente. Aqui está a minha solução, funciona bem para o meu caso de uso, por isso vou deixar aqui, caso alguém encontre este tópico:

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

para throttle ou debounce a melhor maneira é criar um criador de funções para que você possa usá-lo em qualquer lugar, por exemplo:

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

e no seu método de render você pode fazer:

  

o método updateUserProfileField criará uma function separada toda vez que você chamá-lo.

Não tente devolver o manipulador diretamente, por exemplo, isso não funcionará:

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

a razão pela qual isso não funcionará, pois isso gerará uma nova function de aceleração toda vez que o evento for chamado, em vez de usar a mesma function de aceleração; portanto, basicamente, o acelerador será inútil;)

Além disso, se você usar debounce ou throttle , não precisará de setTimeout ou clearTimeout , e é por isso que os usamos: P

Em vez de envolver o handleOnChange em um debounce (), por que não quebrar a chamada do ajax dentro da function callback dentro do debounce, assim não destruindo o object de evento. Então, algo assim:

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

A solução de Julen é meio difícil de ler, aqui está um código de reação mais claro e direto ao ponto para quem o tropeçou com base no título e não nos detalhes minúsculos da pergunta.

tl; versão dr : quando você atualizaria para os observadores, em vez disso, enviaria um método de programação e, por sua vez, notificaria os observadores (ou executaria ajax, etc)

Completar o jsfiddle com o componente de exemplo http://jsfiddle.net/7L655p5L/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 (  ); }, 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) } }, }); 

Você também pode usar um mixin auto-escrito, algo como isto:

 var DebounceMixin = { debounce: function(func, time, immediate) { var timeout = this.debouncedTimeout; if (!timeout) { if (immediate) func(); this.debouncedTimeout = setTimeout(function() { if (!immediate) func(); this.debouncedTimeout = void 0; }.bind(this), time); } } }; 

E então use em seu componente assim:

 var MyComponent = React.createClass({ mixins: [DebounceMixin], handleClick: function(e) { this.debounce(function() { this.setState({ buttonClicked: true }); }.bind(this), 500, true); }, render: function() { return (  ); } }); 

Você não precisa de um grande segmento de código ou de uma gigantesca biblioteca gigantesca como o ReactJS para uma function de aceleração decente. O objective de uma function de aceleração é reduzir os resources do navegador, não aplicar tanta sobrecarga que você está usando ainda mais. Além disso, meus diferentes usos para funções de aceleração exigem muitas circunstâncias diferentes para eles. Aqui está minha lista de coisas que uma ‘boa’ function de acelerador precisa que esta tenha.

  • Sobrecarga mínima.
  • Chamada de function imediata se tiver sido mais do que o intervalo MS desde a última chamada.
  • Evitando executar a function para outro intervalo MS.
  • Atrasar o disparo excessivo de events em vez de descartar completamente o evento.
  • Atualiza o evento atrasado quando necessário, para que ele não fique “obsoleto”.
  • Impede a ação padrão do evento quando a function regulada é atrasada.
  • Ser capaz de remover o ouvinte do ouvinte de evento do acelerador.

E, acredito que a seguinte function do acelerador satisfaz todos eles.

 var cachedThrottleFuncs = [], minimumInterval = 200; // minimum interval between throttled function calls function throttle(func, obj, evt) { var timeouttype = 0, curFunc; function lowerTimeoutType(f){ timeouttype=0; if (curFunc !== undefined){ curFunc(); curFunc = undefined; } }; return cachedThrottleFuncs[ ~( ~cachedThrottleFuncs.indexOf(func) || ~( cachedThrottleFuncs.push(function(Evt) { switch (timeouttype){ case 0: // Execute immediatly ++timeouttype; func.call(Evt.target, Evt); setTimeout(lowerTimeoutType, minimumInterval); break; case 1: // Delayed execute curFunc = func.bind(Evt.target, Evt); Evt.preventDefault(); } }) - 1 ) )]; }; function listen(obj, evt, func){ obj.addEventListener(evt, throttle(func, obj, evt)); }; function mute(obj, evt, func){ obj.removeEventListener(evt, throttle(func, obj, evt)); } 

Exemplo de uso:

 listen(document.body, 'scroll', function whenbodyscrolls(){ if (document.body.scrollTop > 400) mute(document.body, 'scroll', whenbodyscrolls(); else console.log('Body scrolled!') }); 

Como alternativa, se você precisar apenas adicionar ouvintes de events e não precisar remover ouvintes de events, poderá usar a seguinte versão ainda mais simples.

 var minimumInterval = 200; // minimum interval between throttled function calls function throttle(func, obj, evt) { var timeouttype = 0, curEvt = null; function lowerTimeoutType(f){ timeouttype=0; if (curEvt !== null){ func(curEvt); curEvt = null; } }; return function(Evt) { switch (timeouttype){ case 0: // Execute immediately ++timeouttype; // increase the timeouttype func(Evt); // Now, make it so that the timeouttype resets later setTimeout(lowerTimeoutType, minimumInterval); break; case 1: // Delayed execute // make it so that when timeouttype expires, your function // is called with the freshest event curEvt = Evt; Evt.preventDefault(); } }; }; 

Por padrão, isso limita a function para no máximo uma chamada a cada 200 ms. Para alterar o intervalo para um número diferente de milissegundos, basta alterar o valor de minimumInterval .