Esegui il debounce in React.js


496

Come si esegue il debounce in React.js?

Voglio rimandare il handleOnChange.

Ho provato con debounce(this.handleOnChange, 200)ma non funziona.

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

Ho riscontrato lo stesso problema con te, superbe risposte di seguito! ma penso che tu abbia usato un modo sbagliato di farlo debounce. qui, quando onChange={debounce(this.handleOnChange, 200)}/>, invocherà debounce functionogni volta. ma, in realtà, ciò di cui abbiamo bisogno è invocare la funzione restituita dalla funzione di debounce.
pingfengafei,

Risposte:


835

2019: prova gli hook + prometti il ​​rimbalzo

Questa è la versione più aggiornata di come risolverei questo problema. Io userei:

Questo è un cablaggio iniziale ma stai componendo blocchi primitivi da solo e puoi creare il tuo gancio personalizzato in modo da doverlo fare solo una volta.

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

E poi puoi usare il tuo gancio:

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

Troverai questo esempio in esecuzione qui e dovresti leggere la documentazione di reattività asincrona per maggiori dettagli.


2018: prova a promettere il rimbalzo

Vogliamo spesso rimandare le chiamate API per evitare di inondare il back-end con richieste inutili.

Nel 2018, lavorare con i callback (Lodash / Underscore) mi sembra male e soggetto a errori. È facile imbattersi in problemi di plateplate e concorrenza dovuti alla risoluzione delle chiamate API in un ordine arbitrario.

Ho creato una piccola biblioteca con React in mente per risolvere i tuoi dolori: promessa di rimbalzo eccezionale .

Questo non dovrebbe essere più complicato di così:

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 funzione rimbalzata assicura che:

  • Le chiamate API verranno rimosse
  • la funzione rimbalzata restituisce sempre una promessa
  • solo la promessa restituita dell'ultima chiamata verrà risolta
  • un singolo this.setState({ result });accadrà per chiamata API

Alla fine, puoi aggiungere un altro trucco se il componente smonta:

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

Si noti che Observables (RxJS) può anche adattarsi perfettamente al rimbalzo degli input, ma è un'astrazione più potente che può essere più difficile da imparare / usare correttamente.


<2017: vuoi ancora usare il callbo debouncing?

La parte importante qui è creare una singola funzione rimbalzata (o limitata) per istanza del componente . Non si desidera ricreare la funzione di debounce (o acceleratore) ogni volta e non si desidera che più istanze condividano la stessa funzione di debounce.

Non sto definendo una funzione di debouncing in questa risposta in quanto non è realmente pertinente, ma questa risposta funzionerà perfettamente con il carattere _.debouncedi sottolineatura o lodash, nonché con qualsiasi funzione di debouncing fornita dall'utente.


BUONA IDEA:

Poiché le funzioni debounce sono stateful, dobbiamo creare una funzione debounce per istanza del componente .

ES6 (proprietà di classe) : raccomandato

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

ES6 (costruttore di classi)

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

Vedi JsFiddle : 3 istanze producono 1 voce di registro per istanza (che ne rende 3 a livello globale).


Non è una buona idea:

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

Non funzionerà, perché durante la creazione della descrizione della classe dell'oggetto thisnon è l'oggetto stesso creato. this.methodnon restituisce ciò che ti aspetti perché il thiscontesto non è l'oggetto stesso (che in realtà non esiste ancora BTW in quanto è appena stato creato).


Non è una buona idea:

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

Questa volta stai effettivamente creando una funzione rimbalzata che chiama il tuo this.method. Il problema è che lo stai ricreando ad ogni debouncedMethodchiamata, quindi la funzione di rimbalzo appena creata non sa nulla delle chiamate precedenti! È necessario riutilizzare la stessa funzione rimbalzata nel tempo, altrimenti il ​​rimbalzo non avverrà.


Non è una buona idea:

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

Questo è un po 'complicato qui.

Tutte le istanze montate della classe condivideranno la stessa funzione rimbalzata, e molto spesso questo non è quello che vuoi !. Vedi JsFiddle : 3 istanze stanno producendo solo 1 voce di registro a livello globale.

È necessario creare una funzione rimbalzata per ogni istanza del componente e non una singola funzione rimbalzata a livello di classe, condivisa da ciascuna istanza del componente.


Prenditi cura del pool di eventi di React

Ciò è dovuto al fatto che spesso vogliamo rimbalzare o limitare gli eventi DOM.

In React, gli oggetti evento (cioè, SyntheticEvent) che si ricevono nei callback sono raggruppati (questo è ora documentato ). Ciò significa che dopo che è stato chiamato il callback dell'evento, il SyntheticEvent ricevuto verrà rimesso nel pool con attributi vuoti per ridurre la pressione GC.

Pertanto, se si accede alle SyntheticEventproprietà in modo asincrono al callback originale (come può accadere se si accelera / rimbalza), le proprietà a cui si accede potrebbero essere cancellate. Se si desidera che l'evento non venga mai rimesso nel pool, è possibile utilizzare il persist()metodo

Senza persistere (comportamento predefinito: evento aggregato)

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

Il secondo (asincrono) verrà stampato hasNativeEvent=falseperché le proprietà dell'evento sono state ripulite.

Con persistere

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

Il 2 ° (asincrono) verrà stampato hasNativeEvent=trueperchépersist consente di evitare di rimettere l'evento nel pool.

Puoi testare questi 2 comportamenti qui: JsFiddle

Leggi la risposta di Julen per un esempio di utilizzo persist()con una funzione di accelerazione / debounce.


3
Ottima risposta, questo è ottimo per impostare i campi del modulo come "interagenti" per alcuni secondi dopo che hanno smesso di digitare, e quindi essere in grado di annullare l'invio del modulo o onBlur
arush_try.com

8
Nota che in ES6, invece di definire il tuo metodo all'interno del costruttore (sembra strano) puoi fare handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)al massimo livello della tua classe. Stai ancora impostando in modo efficace un membro di istanza ma sembra un po 'più simile a una normale definizione del metodo. Non c'è bisogno di un constructorse non ne hai già definito uno. Suppongo sia principalmente una preferenza di stile.
Thom_nic,

24
Non dimenticare di annullare il metodo debounce in componentWillUnmount: this.method.cancel()- altrimenti potrebbe essere necessario impostareState su un componente non montato.
elado,

4
@JonasKello non puoi rimbalzare all'interno di un componente senza stato perché la funzione debounce è effettivamente stateful. È necessario un componente stateful per contenere quella funzione debounce, ma se necessario è possibile chiamare un componente senza stato con una funzione già debounce.
Sebastien Lorber

2
Perché tutte le risposte includono _.debounce invece di scrivere la funzione? Ha bisogno dell'intera libreria per quella funzione?
chifliiiii,

217

Componenti non controllati

È possibile utilizzare il event.persist()metodo di .

Un esempio segue l'utilizzo di caratteri di sottolineatura _.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} />
    );
  }

});

Modifica: vedi questo JSFiddle


Componenti controllati

Aggiornamento: l'esempio sopra mostra un componente non controllato . Uso sempre elementi controllati, quindi ecco un altro esempio di quanto sopra, ma senza usareevent.persist() "trucco".

È disponibile anche un JSFiddle . Esempio senza trattino basso

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

Modifica: esempi aggiornati e JSFiddles to React 0.12

Modifica: esempi aggiornati per affrontare il problema sollevato da Sebastien Lorber

Modifica: aggiornato con jsfiddle che non usa il trattino basso e usa il debounce javascript semplice.


Questo non funziona per gli input. Il target dell'evento nella funzione debounce non ha più un valore ... quindi l'ingresso rimane vuoto.
Etai,

1
Leggermente complesso, questo. Devi stare un po 'attento agli oggetti di scena. Se impostato <input value={this.props.someprop}..., il rendering non verrà eseguito correttamente poiché l'aggiornamento su Keypress non lo ripristina nel componente fino a dopo il rimbalzo. Va bene omettere value=se si è felici che questo non sia gestito, ma se si desidera precompilare il valore e / o legarlo da qualche altra parte, ovviamente non funziona.
Alastair Maw,

1
@AlastairMaw la domanda aveva un componente incontrollato, ecco perché anche la risposta lo ha. Di seguito ho aggiunto una versione alternativa per i componenti controllati, con un valore precompilato.
luglio

2
questo è molto pericoloso se si montano i tempi multipli dei componenti nel DOM, vedere stackoverflow.com/questions/23123138/…
Sebastien Lorber

4
mentre questa è un'ottima risposta, non consiglio di usarla persistspecialmente quando ci possono essere molti eventi, come su mousemove. Ho visto il codice diventare totalmente insensibile in quel modo. È molto più efficiente estrarre i dati necessari dall'evento nativo nella chiamata all'evento e quindi chiamare la funzione debounce / throttled solo con i dati, NON l'evento stesso. Non è necessario perseguire l'evento in questo modo
MrE

31

2019: utilizzare il gancio di reazione "useCallback"

Dopo aver provato molti approcci diversi, ho scoperto che utilizzare useCallbackera il più semplice ed efficiente per risolvere il problema delle chiamate multiple nell'uso debouncedi un onChangeevento.

Come da documentazione API di Hooks ,

useCallback restituisce una versione memorizzata del callback che cambia solo se una delle dipendenze è cambiata.

Il passaggio di un array vuoto come dipendenza assicura che il callback venga chiamato una sola volta. Ecco una semplice implementazione:

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

Spero che sia di aiuto!


3
Ottima soluzione se stai usando ganci. Mi hai risparmiato molte altre ore di frustrazione. Grazie!
Carl Edwards,

Potresti per favore spiegare perché le chiamate multiple avvengono in primo luogo? Non debounce()considera il onChange()callback lo stesso metodo di callback?
El Anonimo,

Ho modificato questa soluzione per farlo funzionare nella mia app. Per prima cosa ho dovuto spostare la linea const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);all'interno del corpo del componente della funzione o React emette un messaggio di errore sull'uso dell'hook all'esterno di esso. Poi, nel onChangegestore di eventi: <input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}.
El Anonimo il

Ecco come ho usato questa soluzione per consentire all'utente di digitare un input, quindi inviare una chiamata API rimbalzata con il valore di input una volta terminata la digitazione. stackoverflow.com/questions/59358092/… .
El Anonimo,

14

Ho trovato molto utile questo post di Justin Tulk. Dopo un paio di tentativi, in quello che uno percepirebbe come il modo più ufficiale con reagire / redux, mostra che fallisce a causa del pool di eventi sintetici di React . La sua soluzione utilizza quindi uno stato interno per tenere traccia del valore modificato / immesso nell'input, con un callback subito dopo il setStatequale chiama un'azione redux strozzata / rimbalzata che mostra alcuni risultati in tempo reale.

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

14

Se dall'oggetto evento è sufficiente ottenere l'elemento di input DOM, la soluzione è molto più semplice: basta usare ref. Si noti che questo richiede 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} />
    }
}

2
defaultValue è quello che voglio! Grazie mille mach :)
Tazo leladze,

14

Dopo aver lottato con gli input di testo per un po 'e non aver trovato una soluzione perfetta da solo, l'ho trovato su npm: input-debounce-input .

Qui c'è un semplice esempio:

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

Il componente DebounceInput accetta tutti gli oggetti di scena che puoi assegnare a un normale elemento di input. Provalo su codepen

Spero che aiuti anche qualcun altro e li salvi un po 'di tempo.


Dopo aver provato molte soluzioni elencate qui, è stato sicuramente il più semplice.
Vadorequest,

9

Con debounceè necessario mantenere l'evento sintetico originale con event.persist(). Ecco un esempio funzionante testato con 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;

Con il componente funzionale, puoi farlo:

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

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


1
funziona alla grande implementazione migliore che ho trovato finora
Vincent Tang

8

Se stai usando redux puoi farlo in un modo molto elegante con il middleware. È possibile definire un Debouncemiddleware come:

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

È quindi possibile aggiungere il debouncing ai creatori di azioni, come:

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

In realtà c'è già un middleware che puoi ottenere da npm per fare questo per te.


penso che questo middleware debba essere il primo ad essere eseguito in applyMiddleware(...)catena se ne abbiamo molti
Youssef

Il timeout non è inizializzato e il primo clearTimeout avrà a che fare con undefined per un parametro. Non bene.
Jason Rice,

7

Utilizzando ES6 CLASS e React 15.xx & lodash.debounce Im usando i riferimenti di React qui poiché un evento perde questo vincolo 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 ( <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/lodash@4.17.5/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>


7

Molte buone informazioni qui già, ma per essere succinte. Questo funziona per me ...

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

class MyComponent extends Component{
      constructor(props){
        super(props);
        this.handleChange = _.debounce(this.handleChange.bind(this),700);
      }; 

Questo non funziona per me. Lo stato non si aggiorna. Se rimuovo il _debounce wrapper funziona. Adoro questa idea però!
Mote Zart,

Dovrei vedere il tuo codice per offrire molto qui, ma sospetto che ci sia qualcos'altro in corso ... speriamo che questa risposta molto più approfondita farà luce. stackoverflow.com/questions/23123138/…
chad steele,

6

Puoi utilizzare Lodash debounce https://lodash.com/docs/4.17.5#debounce method. È semplice ed 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);
}

È inoltre possibile annullare il metodo di debounce utilizzando il metodo seguente.

this.debounceHandleUpdate.cancel();

Spero che ti aiuti. Saluti!!


5

FYI

Ecco un'altra implementazione PoC:

  • senza librerie (es. lodash) per il rimbalzo
  • utilizzando l'API React Hooks

Spero possa essere d'aiuto :)

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

4

C'è un use-debouncepacchetto che puoi usare con gli hook ReactJS.

Dal README del pacchetto:

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

Come puoi vedere dall'esempio sopra, è impostato per aggiornare la variabile valuesolo una volta al secondo (1000 millisecondi).


3

Solo un'altra variante con recente reazione e 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)} />)
  }
}


3

Hai provato?

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

2

Invece di racchiudere handleOnChange in un debounce (), perché non avvolgere la chiamata ajax all'interno della funzione di callback all'interno del debounce, non distruggendo così l'oggetto evento. Quindi qualcosa del genere:

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

4
Poiché l'oggetto evento non è immutabile e viene distrutto da ReactJS, quindi anche se si avvolge e si ottiene un'acquisizione di chiusura, il codice fallirà.
Henrik,

2

Ecco un esempio che mi è venuto in mente che avvolge un'altra classe con un debouncer. Questo si presta bene ad essere trasformato in un decoratore / funzione di ordine superiore:

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

Ora c'è un'altra soluzione per React e React Native alla fine del 2019 :

reagire-antirimbalzo-componente

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

È un componente, facile da usare, piccolo e widley supportato

Esempio:

inserisci qui la descrizione dell'immagine

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;

* Sono il creatore di questo componente


1

Ero alla ricerca di una soluzione per lo stesso problema e sono imbattuto in questa discussione così come alcuni altri, ma hanno avuto lo stesso problema: se si sta cercando di fare una handleOnChangefunzione ed è necessario il valore da un target dell'evento, si otterrà cannot read property value of nullo qualche tale errore. Nel mio caso, avevo anche bisogno di preservare il contesto thisall'interno della funzione rimbalzata poiché sto eseguendo un'azione fluida. Ecco la mia soluzione, funziona bene per il mio caso d'uso, quindi la lascio qui nel caso in cui qualcuno incontri questo thread:

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

per throttleo debounceil modo migliore è quello di creare un creatore di funzioni in modo da poterlo utilizzare ovunque, ad esempio:

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

e nel tuo rendermetodo puoi fare:

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

il updateUserProfileField metodo creerà una funzione separata ogni volta che la chiami.

Nota : non tentare di restituire direttamente il gestore, ad esempio questo non funzionerà:

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

il motivo per cui questo non funzionerà perché genererà una nuova funzione di accelerazione ogni volta che viene chiamato l'evento invece di utilizzare la stessa funzione di accelerazione, quindi sostanzialmente l'acceleratore sarà inutile;)

Anche se usi debounceo throttlenon hai bisogno setTimeouto clearTimeout, questo è il motivo per cui li usiamo: P


1

Ecco uno snippet che utilizza l'approccio di @ Abra racchiuso in un componente di funzione (usiamo fabric per l'interfaccia utente, basta sostituirlo con un semplice pulsante)

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

1

La mia soluzione è basata su hook (scritta in Typescript).

Ho 2 ganci principali useDebouncedValueeuseDebouncedCallback

Primo - useDebouncedValue

Supponiamo di avere una casella di ricerca, ma vogliamo chiedere al server i risultati della ricerca dopo che l'utente ha smesso di digitare per 0,5 secondi

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

Implementazione

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

Secondo useDebouncedCallback

Crea semplicemente una funzione "debounce" nell'ambito del componente.

Supponiamo di avere un componente con un pulsante che mostrerà l'avviso 500ms dopo che hai smesso di cliccarlo.

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

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

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

Implementazione (nota che sto usando lodash / debounce come aiuto)

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

0

Ecco un esempio di TypeScript funzionante per coloro che usano TS e vogliono rimbalzare le asyncfunzioni.

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

un po 'tardi qui, ma questo dovrebbe aiutare. crea questa classe (è scritta in dattiloscritto ma è facile convertirla in 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;
}

e da usare

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

0

puoi usare 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');

0

La soluzione di Julen è difficile da leggere, ecco un codice di reazione più chiaro e puntuale per chiunque lo abbia inciampato in base al titolo e non ai piccoli dettagli della domanda.

tl; versione dr : quando si aggiorna agli osservatori invia una chiamata invece un metodo di pianificazione e che a sua volta avviserà effettivamente gli osservatori (o eseguirà ajax, ecc.)

Jsfiddle completo con jsfiddle componente di esempio

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

});

3
Questo non renderà lo stato / i tempi del debounce globali in tutte le istanze di InputField, perché è stato creato con la definizione della classe? Forse è quello che vuoi, ma vale la pena notare a prescindere.
deruba il

1
pericoloso se montato più volte nella dom, controlla stackoverflow.com/questions/23123138/…
Sebastien Lorber

2
Questa è una cattiva soluzione, a causa di problemi di doppio montaggio: stai facendo la tua funzione per programmareCambia un singleton e non è una buona idea. -1
Henrik l'

0

Evita di usare event.persist(): vuoi che React ricicli l'evento sintetico. Penso che il modo più pulito sia che usi le classi o gli hook sia di dividere il callback in due parti:

  1. Il callback senza debouncing
  2. Chiama una funzione rimbalzata con solo i pezzi dell'evento di cui hai bisogno (quindi l'evento sintetico può essere riciclato)

Classi

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

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

<div onMouseOver={this.onMouseOver} />

funzioni

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

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

<div onMouseOver={this.onMouseOver} />

Nota che se la tua handleMouseOverfunzione usa lo stato all'interno del componente, dovresti usare useMemoinvece useRefe passare quelli come dipendenze, altrimenti lavorerai con dati non aggiornati (ovviamente non si applicano alle classi).


0

Estendere l'uso Hook di stato

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;

Usa il gancio

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/


0

Ho incontrato questo problema oggi. Risolto usando setTimeoute clearTimeout.

Farò un esempio che potresti adattare:

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

Puoi anche rifattorarlo nel suo componente di funzione:

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

E usalo come:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.