Realizar debounce en React.js

496

¿Cómo se realiza el rebote en React.js?

Quiero renunciar al handleOnChange.

Lo intenté debounce(this.handleOnChange, 200)pero no 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 <input type="search" name="p" onChange={this.handleOnChange} />;
  },

  handleOnChange: function(event) {
    // make ajax call
  }
});
Chetan Ankola
fuente
Me encontré con el mismo problema con usted, excelentes respuestas a continuación! Pero creo que utilizó de manera incorrecta debounce. aquí, cuando onChange={debounce(this.handleOnChange, 200)}/>invocará debounce functioncada vez. pero, de hecho, lo que necesitamos es invocar la función que devolvió la función antirrebote.
pingfengafei

Respuestas:

835

2019: prueba ganchos + promesa de rebote

Esta es la versión más actualizada de cómo resolvería este problema. Yo usaría:

Este es un cableado inicial, pero está componiendo bloques primitivos por su cuenta, y puede hacer su propio gancho personalizado para que solo necesite hacer esto una vez.

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

Y luego puedes usar tu gancho:

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

Encontrará este ejemplo ejecutándose aquí y debe leer la documentación de react-async-hook para obtener más detalles.


2018: prueba la promesa de rebote

A menudo queremos eliminar las llamadas a la API para evitar inundar el backend con solicitudes inútiles.

En 2018, trabajar con devoluciones de llamada (Lodash / Underscore) se siente mal y propenso a errores. Es fácil encontrar problemas repetitivos y de simultaneidad debido a que las llamadas API se resuelven en un orden arbitrario.

He creado una pequeña biblioteca con React en mente para resolver tus dolores: awesome-debounce-promise .

Esto no debería ser más complicado que eso:

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 función antirrebote asegura que:

  • Las llamadas a la API serán eliminadas
  • la función eliminada siempre devuelve una promesa
  • solo se resolverá la promesa devuelta de la última llamada
  • this.setState({ result });sucederá un solo por llamada API

Eventualmente, puede agregar otro truco si su componente se desmonta:

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

Tenga en cuenta que los Observables (RxJS) también pueden ser una excelente opción para eliminar las entradas, pero es una abstracción más poderosa que puede ser más difícil de aprender / usar correctamente.


<2017: ¿todavía quieres usar la devolución de llamada?

La parte importante aquí es crear una sola función sin rebote (o estrangulada) por instancia de componente . No desea volver a crear la función antirrebote (o acelerador) cada vez, y no desea que ninguna de las instancias múltiples comparta la misma función antirrebote.

No estoy definiendo una función de eliminación de rebotes en esta respuesta, ya que no es realmente relevante, pero esta respuesta funcionará perfectamente bien con _.debouncesubrayado o lodash, así como con cualquier función de eliminación de rebotes proporcionada por el usuario.


BUENA IDEA:

Debido a que las funciones sin rebote tienen estado, tenemos que crear una función sin rebote por instancia de componente .

ES6 (propiedad de clase) : recomendado

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

ES6 (constructor de clase)

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

Ver JsFiddle : 3 instancias están produciendo 1 entrada de registro por instancia (eso hace 3 globalmente).


No es Buena idea:

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

No funcionará, porque durante la creación del objeto de descripción de clase, thisno se crea el objeto en sí mismo. this.methodno devuelve lo que espera porque el thiscontexto no es el objeto en sí (que en realidad aún no existe, por cierto, ya que se está creando).


No es Buena idea:

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

Esta vez está creando efectivamente una función sin rebote que llama a su this.method. ¡El problema es que lo está recreando en cada debouncedMethodllamada, por lo que la función antirrebote recién creada no sabe nada sobre las llamadas anteriores! Debe volver a usar la misma función de eliminación de rebote con el tiempo o la eliminación de rebote no sucederá.


No es Buena idea:

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

Esto es un poco complicado aquí.

¡Todas las instancias montadas de la clase compartirán la misma función sin rebote, y la mayoría de las veces esto no es lo que quieres! Consulte JsFiddle : 3 instancias solo producen 1 entrada de registro a nivel mundial.

Debe crear una función sin rebote para cada instancia de componente , y no una sola función sin rebote a nivel de clase, compartida por cada instancia de componente.


Cuida la agrupación de eventos de React

Esto está relacionado porque a menudo queremos eliminar el rebote o limitar los eventos DOM.

En React, los objetos de evento (es decir, SyntheticEvent) que recibe en devoluciones de llamada se agrupan (esto ahora está documentado ). Esto significa que después de que se haya llamado la devolución de llamada del evento, el SyntheticEvent que reciba se volverá a colocar en el grupo con atributos vacíos para reducir la presión del GC.

Por lo tanto, si accede a las SyntheticEventpropiedades de forma asincrónica a la devolución de llamada original (como puede ser el caso si acelera / elimina el rebote), las propiedades a las que accede pueden borrarse. Si desea que el evento nunca se vuelva a colocar en el grupo, puede usar el persist()método.

Sin persistencia (comportamiento predeterminado: evento agrupado)

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

El segundo (asíncrono) se imprimirá hasNativeEvent=falseporque las propiedades del evento se han limpiado.

Con persistir

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

El segundo (asíncrono) se imprimirá hasNativeEvent=trueporque le persistpermite evitar volver a colocar el evento en el grupo.

Puede probar estos 2 comportamientos aquí: JsFiddle

Lea la respuesta de Julen para ver un ejemplo de uso persist()con una función de aceleración / antirrebote.

Sebastien Lorber
fuente
3
Excelente respuesta, esto es excelente para configurar el estado de los campos del formulario como 'interactuando' durante unos segundos después de que dejan de escribir y luego poder cancelar en el envío del formulario o en
Blur
8
Tenga en cuenta que en ES6, en lugar de definir su método dentro del constructor (se siente raro), puede hacerlo handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)en el nivel superior de su clase. Todavía está configurando efectivamente un miembro de instancia, pero se parece un poco más a una definición de método normal. No hay necesidad de un constructorsi aún no tiene uno definido. Supongo que es principalmente una preferencia de estilo.
thom_nic
24
No olvide cancelar el método sin rebote en componentWillUnmount: this.method.cancel()- de lo contrario, es posible que desee establecer State en un componente desmontado.
elado
44
@JonasKello no se puede eliminar el rebote dentro de un componente sin estado porque la función sin rebote es realmente con estado. Necesita un componente con estado para mantener esa función sin rebote, pero puede llamar a un componente sin estado con una función ya sin rebote si es necesario.
Sebastien Lorber
2
¿Por qué toda respuesta incluye _.debounce en lugar de escribir la función? ¿Necesita toda la biblioteca para esa función?
chifliiiii
217

Componentes no controlados

Puedes usar el event.persist()método .

Sigue un ejemplo usando guiones bajos _.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} />
    );
  }

});

Editar: Ver este JSFiddle


Componentes controlados

Actualización: el ejemplo anterior muestra un componente no controlado . Uso elementos controlados todo el tiempo, así que aquí hay otro ejemplo de lo anterior, pero sin usar el event.persist()"truco".

Un JSFiddle también está disponible . Ejemplo sin guión bajo

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

Editar: ejemplos actualizados y JSFiddles to React 0.12

Editar: ejemplos actualizados para abordar el problema planteado por Sebastien Lorber

Editar: actualizado con jsfiddle que no usa guión bajo y usa JavaScript antirrebote.

julen
fuente
Esto no funciona para las entradas. El destino del evento en la función de rebote ya no tiene un valor ... por lo que la entrada permanece vacía.
Etai
1
Ligeramente complejo, esto. Tienes que tener un poco de cuidado con los accesorios. Si lo configura <input value={this.props.someprop}..., no se representará correctamente, ya que la actualización al presionar la tecla no vuelve al componente hasta después del rechazo. Está bien omitir value=si está contento de que esto no se gestione, pero si desea rellenar previamente el valor y / o vincularlo en otro lugar, obviamente esto no funciona.
Alastair Maw
1
@AlastairMaw la pregunta tenía un componente incontrolado, por eso la respuesta también lo tiene. A continuación, agregué una versión alternativa para componentes controlados, con un valor rellenado previamente.
julen
2
esto es muy peligroso si monta el componente varias veces en el DOM, consulte stackoverflow.com/questions/23123138/…
Sebastien Lorber el
44
Si bien esta es una gran respuesta, no recomiendo usar persistespecialmente cuando puede haber muchos eventos, como en mousemove. He visto que el código deja de responder de esa manera. Es mucho más eficiente extraer los datos necesarios del evento nativo en la llamada del evento, y luego llamar a la función sin rebote / estrangulada solo con los datos, NO el evento en sí. No es necesario que persista el evento de esa manera
MrE
31

2019: use el gancho de reacción 'useCallback'

Después de probar muchos enfoques diferentes, descubrí que usar useCallbackes el más simple y más eficiente para resolver el problema de llamadas múltiples de usar debouncedentro de un onChangeevento.

Según la documentación de la API de Hooks ,

useCallback devuelve una versión memorizada de la devolución de llamada que solo cambia si una de las dependencias ha cambiado.

Pasar una matriz vacía como dependencia asegura que la devolución de llamada se llame solo una vez. Aquí hay una implementación simple:

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

const handler = useCallback(debounce(someFunction, 2000), []);

const onChange = (event) => {
    // perform any event related action here

    handler();
 };

¡Espero que esto ayude!

Sameer Ingavale
fuente
3
Excelente solución si estás usando ganchos. Me salvaste muchas más horas de frustración. ¡Gracias!
Carl Edwards el
¿Podría explicar por qué las llamadas múltiples ocurren en primer lugar? ¿ debounce()No considera que la onChange()devolución de llamada sea el mismo método de devolución de llamada?
El Anónimo el
Modifiqué esta solución para que funcione en mi aplicación. Primero tuve que mover la línea const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);dentro del cuerpo del componente de función o React emite un mensaje de error sobre el uso del gancho fuera de él. Luego, en el onChangecontrolador de eventos: <input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}.
El Anónimo el
Así es como usé esta solución para permitir que el usuario escriba en una entrada y luego envíe una llamada API sin rebote con el valor de entrada una vez que haya terminado de escribir. stackoverflow.com/questions/59358092/… .
El Anónimo
14

Encontré esta publicación de Justin Tulk muy útil. Después de un par de intentos, en lo que uno percibiría como la forma más oficial con react / redux, muestra que falla debido a la agrupación de eventos sintéticos de React . Su solución luego usa un estado interno para rastrear el valor cambiado / ingresado en la entrada, con una devolución de llamada justo después de lo setStatecual llama a una acción redux acelerada / sin rebote que muestra algunos resultados en tiempo 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 (
        <TextField
            className = {styles.field}
            onChange = {this.handleChange}
            value = {this.props.value}
        />
    )
  }
}
racémico
fuente
14

Si todo lo que necesita del objeto de evento es obtener el elemento de entrada DOM, la solución es mucho más simple: solo use ref. Tenga en cuenta que esto requiere subrayado :

class Item extends React.Component {
    constructor(props) {
        super(props);
        this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000);
    }
    saveTitle(){
        let val = this.inputTitle.value;
        // make the ajax call
    }
    render() {
        return <input 
                    ref={ el => this.inputTitle = el } 
                    type="text" 
                    defaultValue={this.props.title} 
                    onChange={this.saveTitle} />
    }
}
Yura
fuente
2
defaultValue es lo que quiero! Gracias muy mach :)
Tazo leladze
14

Después de luchar con las entradas de texto por un tiempo y no encontrar una solución perfecta por mi cuenta, encontré esto en npm: react-debounce-input .

Aquí hay un ejemplo 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);

El componente DebounceInput acepta todos los accesorios que puede asignar a un elemento de entrada normal. Pruébalo en codepen

Espero que también ayude a alguien más y les ahorre algo de tiempo.

Hooman Askari
fuente
Después de probar muchas de las soluciones enumeradas aquí, definitivamente fue la más fácil.
Vadorequest
9

Con debounceusted, debe mantener el evento sintético original event.persist(). Aquí está el ejemplo de trabajo probado 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 el componente funcional, puede hacer esto:

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

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

Mohan Dere
fuente
1
funciona la mejor implementación que encontré hasta ahora
Vincent Tang
8

Si está utilizando redux, puede hacerlo de una manera muy elegante con middleware. Puede definir un Debouncemiddleware 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)
  }
}

A continuación, puede agregar eliminación de rebotes a los creadores de acciones, como:

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

En realidad, ya hay middleware que puede usar npm para hacer esto por usted.

Mate
fuente
Creo que este middleware debe ser el primero en ejecutarse en applyMiddleware(...)cadena si tenemos muchos
Youssef
El tiempo de espera no está inicializado y ese primer clearTimeout se ocupará de indefinido para un parámetro. No está bien.
Jason Rice
7

Usando ES6 CLASS y React 15.xx & lodash.debounce Estoy usando las referencias de React aquí ya que el evento pierde el enlace interno.

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>

ACERO
fuente
7

Mucha información buena aquí ya, pero para ser sucinto. Esto funciona para mi ...

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

class MyComponent extends Component{
      constructor(props){
        super(props);
        this.handleChange = _.debounce(this.handleChange.bind(this),700);
      }; 
chad steele
fuente
Esto no funciona para mi. El estado no se actualiza. Si elimino el _debounce contenedor, funciona. ¡Aunque me encanta esta idea!
Mote Zart
Tendría que ver su código para ofrecer mucho aquí, pero sospecho que está sucediendo algo más ... espero que esta respuesta mucho más exhaustiva arroje algo de luz. stackoverflow.com/questions/23123138/…
chad steele
6

Puede usar el método Lodash debounce https://lodash.com/docs/4.17.5#debounce . Es simple y efectivo.

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

También puede cancelar el método antirrebote utilizando el siguiente método.

this.debounceHandleUpdate.cancel();

Espero que te ayude. ¡¡Salud!!

Dinesh Madhanlal
fuente
5

FYI

Aquí hay otra implementación de PoC:

  • sin ninguna biblioteca (por ejemplo, lodash) para eliminar el rebote
  • usando la API React Hooks

Espero que ayude :)

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

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

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

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

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

Hay un use-debouncepaquete que puede usar con los ganchos ReactJS.

Del paquete README:

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

Como puede ver en el ejemplo anterior, está configurado para actualizar la variable valuesolo una vez por segundo (1000 milisegundos).

Arte
fuente
3

Solo otra variante con reacción reciente y 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)} />)
  }
}
puchu
fuente
3

Una solución agradable y limpia, que no requiere dependencias externas:

Debouncing con React Hooks

Utiliza un personalizado más los ganchos useEffect React y el método setTimeout/ clearTimeout.

Bruno Silvano
fuente
3

¿Has probado?

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

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

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

En lugar de envolver el handleOnChange en un debounce (), ¿por qué no envolver la llamada ajax dentro de la función de devolución de llamada dentro del debounce, sin destruir el objeto de evento? Entonces algo como esto:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}
Robert
fuente
44
Debido a que el objeto de evento no es inmutable y ReactJS lo destruye, por lo que incluso si ajusta y logra una captura de cierre, el código fallará.
Henrik
2

Aquí hay un ejemplo que se me ocurrió que envuelve otra clase con un rebotador. Esto se presta muy bien para convertirse en una función de decorador / orden 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 <Thingy {...restOfProps} {...this.state} />
    }
}
mlucool
fuente
2

Ahora hay otra solución para React y React Native a fines de 2019 :

componente react-debounce

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

Es un componente, fácil de usar, pequeño y compatible con Widley

Ejemplo:

ingrese la descripción de la imagen aquí

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;

* Soy el creador de este componente

Rebs
fuente
1

Estaba buscando una solución para el mismo problema y encontré este hilo así como algunos otros, pero tenían el mismo problema: si está tratando de hacer una handleOnChangefunción y necesita el valor de un objetivo de evento, obtendrá cannot read property value of nullo algo tal error En mi caso, también necesitaba preservar el contexto thisdentro de la función eliminada ya que estoy ejecutando una acción fluible. Aquí está mi solución, funciona bien para mi caso de uso, así que la dejo aquí en caso de que alguien se encuentre con este hilo:

// 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)
Eduardo
fuente
1

para throttleo debouncela mejor manera es crear un creador de funciones para que pueda usarlo en cualquier lugar, por ejemplo:

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

y en tu rendermétodo puedes hacer:

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

la updateUserProfileField método creará una función separada cada vez que lo llame.

Tenga en cuenta que no intente devolver el controlador directamente, por ejemplo, esto no funcionará:

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

la razón por la cual esto no funcionará porque generará una nueva función de aceleración cada vez que se llame el evento en lugar de usar la misma función de aceleración, por lo que básicamente la aceleración será inútil;)

Además, si usa debounceo throttleno necesita setTimeouto clearTimeout, esta es realmente la razón por la que los usamos: P

Alnamrouti Fareed
fuente
1

Aquí hay un fragmento que utiliza el enfoque de @ Abra envuelto en un componente de función (usamos fabric para la interfaz de usuario, solo reemplácelo con un botón simple)

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}
        />
    );
}
Hilo Pitt
fuente
1

Mi solución está basada en ganchos (escrita en mecanografiado).

Tengo 2 ganchos principales useDebouncedValueyuseDebouncedCallback

Primero - useDebouncedValue

Digamos que tenemos un cuadro de búsqueda, pero queremos pedirle al servidor los resultados de búsqueda después de que el usuario haya dejado de escribir durante 0,5 segundos.

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

Implementación

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

Segundo useDebouncedCallback

Simplemente crea una función 'sin rebote' en el alcance de su componente.

Digamos que tenemos un componente con un botón que mostrará la alerta 500ms después de que dejaste de hacer clic.

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

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

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

Implementación (tenga en cuenta que estoy usando lodash / debounce como ayudante)

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

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

  return debouncedCallback;
}
pie6k
fuente
0

Aquí hay un ejemplo de TypeScript que funciona para aquellos que usan TS y desean eliminar las asyncfunciones.

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

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

un poco tarde aquí, pero esto debería ayudar. crear esta clase (está escrito en mecanografiado pero es fácil convertirlo a 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;
}

y para usar

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

puedes usar tlence tlence

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

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

La solución de Julen es un poco difícil de leer, aquí hay un código más claro y directo para cualquiera que lo haya tropezado según el título y no los pequeños detalles de la pregunta.

tl; versión dr : cuando actualizaría a los observadores, envíe una llamada a un método de programación y eso a su vez notificará a los observadores (o realizará ajax, etc.)

Complete jsfiddle con el componente de ejemplo jsfiddle

var InputField = React.createClass({

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

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

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

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

        this.scheduleChange();
    },

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

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

});
srcspider
fuente
3
¿Esto no hará que el estado / sincronización del rebote sea global en todas las instancias de InputField, porque se creó con la definición de clase? Tal vez eso es lo que quieres, pero vale la pena señalarlo independientemente.
Robbles
1
peligroso si se monta varias veces en el dom, consulte stackoverflow.com/questions/23123138/…
Sebastien Lorber el
2
Esta es una mala solución, debido a problemas de doble montaje: está haciendo su función para programar Cambiar un singleton y eso no es una buena idea. -1
Henrik
0

Evite usar event.persist(): desea dejar que React recicle el evento sintético. Creo que la forma más limpia de usar clases o ganchos es dividir la devolución de llamada en dos partes:

  1. La devolución de llamada sin rebote
  2. Llama a una función eliminada con solo las partes del evento que necesita (para que el evento sintético pueda reciclarse)

Clases

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

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

<div onMouseOver={this.onMouseOver} />

Las funciones

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

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

<div onMouseOver={this.onMouseOver} />

Tenga en cuenta que si su handleMouseOverfunción usa el estado desde el componente, debe usarlo en useMemolugar de useRefy pasarlos como dependencias; de lo contrario, trabajará con datos obsoletos (no se aplica a las clases, por supuesto).

Dominic
fuente
0

Ampliar uso Gancho de estado

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 gancho

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

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

Micah B.
fuente
0

Encontré este problema hoy. Lo resolvió usando setTimeouty clearTimeout.

Le daré un ejemplo que podría adaptar:

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

También puede refactorizarlo en su propio componente de función:

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

Y úsalo como:

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

class PlacesAutocomplete extends Component {
  debounceTimer = null;

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

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

export default PlacesAutocomplete
Francisco Hanna
fuente