Executar debounce no React.js

496

Como você executa debounce no React.js?

Eu quero renunciar ao 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 <input type="search" name="p" onChange={this.handleOnChange} />;
  },

  handleOnChange: function(event) {
    // make ajax call
  }
});
Chetan Ankola
fonte
Eu encontrei o mesmo problema com você, respostas excelentes abaixo!, mas acho que você usou o caminho errado debounce. aqui, quando onChange={debounce(this.handleOnChange, 200)}/>, ele será invocado debounce functionsempre. mas, de fato, o que precisamos é chamar a função que função de retorno retornou.
Pingfengafei

Respostas:

835

2019: experimente ganchos + prometer debouncing

Esta é a versão mais atualizada de como eu resolveria esse problema. Eu usaria:

Essa é uma fiação inicial, mas você está compondo blocos primitivos por conta própria e pode criar seu próprio gancho personalizado para que você precise fazer isso apenas uma 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,
  };
};

E então você pode usar seu 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>
  );
};

Você encontrará este exemplo em execução aqui e deve ler a documentação do react-async-hook para obter mais detalhes.


2018: tente prometer renúncia

Muitas vezes, queremos rejeitar chamadas de API para evitar inundar o back-end com solicitações inúteis.

Em 2018, trabalhar com retornos de chamada (Lodash / Underscore) parece ruim e propenso a erros. É fácil encontrar problemas de clichê e simultaneidade devido à resolução de chamadas da API em uma ordem arbitrária.

Eu criei uma pequena biblioteca com o React em mente para resolver suas dores: awesome-debounce-promessa .

Isso não deve ser mais complicado do que isso:

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

A função debounce garante que:

  • As chamadas de API serão debitadas
  • a função debugada sempre retorna uma promessa
  • apenas a promessa retornada da última chamada será resolvida
  • um único this.setState({ result });acontecerá por chamada da API

Eventualmente, você pode adicionar outro truque se o seu componente desmontar:

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

Observe que os Observables (RxJS) também podem ser ótimos para rebater entradas, mas é uma abstração mais poderosa que pode ser mais difícil de aprender / usar corretamente.


<2017: ainda deseja usar a conversão de retorno de chamada?

A parte importante aqui é criar uma única função rejeitada (ou estrangulada) por instância do componente . Você não deseja recriar a função debounce (ou aceleração) toda vez e não deseja que várias instâncias compartilhem a mesma função debounce.

Não estou definindo uma função de depuração nesta resposta, pois ela não é realmente relevante, mas ela funcionará perfeitamente com _.debouncesublinhado ou lodash, além de qualquer função de depuração fornecida pelo usuário.


BOA IDEIA:

Como as funções debitadas são com estado, precisamos criar uma função debitada por instância do componente .

ES6 (propriedade da classe) : recomendado

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

ES6 (construtor de classe)

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

Consulte JsFiddle : 3 instâncias estão produzindo 1 entrada de log por instância (que gera 3 globalmente).


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 objeto de descrição da classe, thisnão é o próprio objeto criado. this.methodnão retorna o que você espera, porque o thiscontexto não é o objeto em si (que na verdade ainda não existe ainda, pois 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 função rejeitada que chama seu this.method. O problema é que você a está recriando a cada debouncedMethodchamada, para que a função de rebounce recém-criada não saiba nada sobre chamadas anteriores! Você deve reutilizar a mesma função debatida ao longo do tempo ou a devolução não ocorrerá.


Não é uma boa ideia:

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

Isso é um pouco complicado aqui.

Todas as instâncias montadas da classe compartilharão a mesma função rejeitada e, na maioria das vezes, não é isso que você deseja! Consulte JsFiddle : 3 instâncias estão produzindo apenas 1 entrada de log globalmente.

É necessário criar uma função debounce para cada instância do componente , e não uma única função debounce no nível da classe, compartilhada por cada instância do componente.


Cuide do pool de eventos do React

Isso está relacionado porque geralmente queremos rebater ou limitar os eventos do DOM.

No React, os objetos de evento (ou seja, SyntheticEvent) que você recebe nos retornos de chamada são agrupados (isso agora está documentado ). Isso significa que depois que o retorno de chamada do evento for chamado, o SyntheticEvent que você recebe será colocado de volta no pool com atributos vazios para reduzir a pressão do GC.

Portanto, se você acessar SyntheticEventpropriedades de forma assíncrona com o retorno de chamada original (como pode ser o caso se você acelerar / devolver), as propriedades que você acessar poderão ser apagadas. Se você deseja que o evento nunca seja colocado de volta no pool, use o persist()método

Sem persistir (comportamento padrão: evento em pool)

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

O segundo (assíncrono) será impresso 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 (assíncrono) será impresso hasNativeEvent=trueporquepersist permite evitar colocar o evento de volta no pool.

Você pode testar estes 2 comportamentos aqui: JsFiddle

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

Sebastien Lorber
fonte
3
Resposta soberba, isso é ótimo para a criação do estado campos do formulário como 'interagir' durante alguns segundos depois que parar de digitar e, em seguida, ser capaz de cancelar no formulário de envio ou onBlur
arush_try.com
8
Observe que no ES6, em vez de definir seu método dentro do construtor (parece estranho), você pode fazer handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)no nível superior da sua classe. Você ainda está configurando efetivamente um membro da instância, mas parece um pouco mais com uma definição de método normal. Não há necessidade de um constructorse você ainda não tiver um definido. Suponho que seja principalmente uma preferência de estilo.
thom_nic
24
Não se esqueça de cancelar o método debuted em componentWillUnmount: this.method.cancel()- caso contrário, ele pode querer definirState em um componente desmontado.
Elado
4
@JonasKello, você não pode rebater dentro de um componente sem estado, porque a função rebocada é realmente com estado. Você precisa de um componente com estado para manter essa função debocada, mas pode chamar um componente sem estado com uma função já debitada, se necessário.
Sebastien Lorber
2
Por que todas as respostas incluem _.debounce em vez de escrever a função? Ele precisa de toda a biblioteca para essa função?
chifliiiii
217

Componentes não controlados

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

Um exemplo segue usando sublinhado _.debounce():

var SearchBox = React.createClass({

  componentWillMount: function () {
     this.delayedCallback = _.debounce(function (event) {
       // `event.target` is accessible now
     }, 1000);
  },

  onChange: function (event) {
    event.persist();
    this.delayedCallback(event);
  },

  render: function () {
    return (
      <input type="search" onChange={this.onChange} />
    );
  }

});

Edit: 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 acima, mas sem usar oevent.persist() "truque".

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 (
        <input type="search"
               value={this.state.query}
               onChange={this.onChange} />
      );
    }
});


var Search = React.createClass({
    getInitialState: function () {
        return {
            result: this.props.query
        };
    },

    handleSearch: function (query) {
        this.setState({result: query});
    },

    render: function () {
      return (
        <div id="search">
          <SearchBox query={this.state.result}
                     handleSearch={this.handleSearch} />
          <p>You searched for: <strong>{this.state.result}</strong></p>
        </div>
      );
    }
});

React.render(<Search query="Initial query" />, document.body);

Edit: exemplos atualizados e JSFiddles para reagir 0.12

Edit: exemplos atualizados para abordar a questão levantada por Sebastien Lorber

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

julen
fonte
Isso não funciona para entradas. O destino do evento na função rejeitada não tem mais um valor ... então a entrada permanece vazia.
Etai 29/07
1
Um pouco complexo, isso. Você precisa ter um pouco de cuidado com os adereços. Se você definir <input value={this.props.someprop}..., ele não será renderizado corretamente, pois a atualização ao pressionar a tecla não voltará ao componente até depois da devolução. Não há problema em omitir value=se você está feliz por isso não ser gerenciado, mas se você deseja preencher previamente o valor e / ou vinculá-lo em outro lugar, obviamente isso não funciona.
Alastair Maw
1
@AlastairMaw a pergunta tinha um componente não controlado, é por isso que a resposta também. Adicionei abaixo uma versão alternativa para componentes controlados, com um valor pré-preenchido.
Julen
2
isso é muito perigoso se você montar o componente várias vezes no DOM, consulte stackoverflow.com/questions/23123138/…
Sebastien Lorber
4
Embora essa seja uma ótima resposta, não recomendo o uso, persistespecialmente quando houver muitos eventos, como no mousemove. Eu vi o código ficar totalmente sem resposta dessa maneira. É muito mais eficiente extrair os dados necessários do evento nativo na chamada de evento e, em seguida, chamar a função debounce / throttled apenas com os dados, NÃO com o próprio evento. Não há necessidade de persistir o evento que maneira
MRE
31

2019: use o gancho de reação 'useCallback'

Depois de tentar várias abordagens diferentes, achei o uso useCallbacko mais simples e eficiente na solução do problema de várias chamadas do uso debounceem um onChangeevento.

De acordo com a documentação da API Hooks ,

useCallback retorna uma versão memorizada do retorno de chamada que muda apenas se uma das dependências for alterada.

Passar uma matriz vazia como uma dependência garante que o retorno de chamada seja chamado apenas uma vez. Aqui está uma implementação simples:

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 isto ajude!

Sameer Ingavale
fonte
3
Excelente solução se você estiver usando ganchos. Você me salvou muito mais horas de frustração. Obrigado!
Carl Edwards
Você poderia explicar por que as várias chamadas acontecem em primeiro lugar? Não debounce()considera o onChange()retorno de chamada o mesmo método de retorno de chamada?
El Anonimo
Modifiquei esta solução para que ela funcionasse no meu aplicativo. Primeiro tive que mover a linha const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);dentro do corpo do componente de função ou o React gera uma mensagem de erro sobre o uso do gancho fora dele. Em seguida, no onChangemanipulador de eventos: <input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}.
El Anonimo
Aqui está como eu usei essa solução para permitir que o usuário digite uma entrada e, em seguida, envie uma chamada de API com o valor de entrada assim que terminar de digitar. stackoverflow.com/questions/59358092/… .
El Anonimo
14

Achei este post de Justin Tulk muito útil. Depois de algumas tentativas, da maneira que alguém perceberia ser a maneira mais oficial com o react / redux, isso mostra que falha devido ao pool de eventos sintéticos do React . Sua solução então usa algum estado interno para rastrear o valor alterado / inserido na entrada, com um retorno de chamada logo após o setStatequal chama uma ação de redux acelerada / desacelerada 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 (
        <TextField
            className = {styles.field}
            onChange = {this.handleChange}
            value = {this.props.value}
        />
    )
  }
}
racêmico
fonte
14

Se tudo o que você precisa no objeto de evento é obter o elemento de entrada DOM, a solução é muito mais simples - basta usar ref. Observe que isso requer sublinhado :

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
fonte
2
defaultValue é o que eu quero! Obrigado muito mach :)
Tazo leladze
14

Depois de lutar com as entradas de texto por um tempo e sem encontrar uma solução perfeita sozinha, encontrei isso no npm: 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 (
    <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);

O componente DebounceInput aceita todos os acessórios que você pode atribuir a um elemento de entrada normal. Experimente no codepen

Espero que ajude outra pessoa também e economize algum tempo.

Hooman Askari
fonte
Depois de tentar muitas soluções listadas aqui, definitivamente foi a mais fácil.
Vadorequest
9

Com debouncevocê, é necessário manter o evento sintético original por perto event.persist(). Aqui está um exemplo de trabalho testado com 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;

Com o componente funcional, você pode fazer isso -

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

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

Mohan Dere
fonte
1
funciona muito melhor implementação i encontrado até agora
Vincent Tang
8

Se você estiver usando redux, poderá fazer isso de uma maneira muito elegante com o middleware. Você pode definir um 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)
  }
}

Em seguida, você pode adicionar a renúncia aos criadores de ações, como:

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

Na verdade, existe um middleware que você pode usar para usar o npm.

Matt
fonte
eu acho que isso middleware deve ser o primeiro a ser executado em applyMiddleware(...)cadeia, se temos muitos
Youssef
O tempo limite não é inicializado e o primeiro clearTimeout estará lidando com indefinido para um parâmetro. Não é bom.
Jason Rice
7

Usando ES6 CLASS e Reagir 15.xx & lodash.debounce Im usando de Reagir refs aqui desde as perdas de eventos a este ligam 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/[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>

AÇO
fonte
7

Muita informação boa aqui já, mas para ser sucinta. Isso funciona para mim ...

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
fonte
Isso não funciona para mim. O estado não é atualizado. Se eu remover o _debounce invólucro, ele funciona. Eu amo essa idéia!
Mote Zart
Eu precisaria ver o seu código para oferecer muito aqui, mas suspeito que algo mais esteja acontecendo ... espero que esta resposta muito mais completa elucide alguma luz. stackoverflow.com/questions/23123138/…
chad steele 13/04/19
6

Você pode usar o método https://lodash.com/docs/4.17.5#debounce do Lodash debounce . É 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 rejeição usando o método abaixo.

this.debounceHandleUpdate.cancel();

Espero que ajude você. Felicidades!!

Dinesh Madhanlal
fonte
5

Para sua informação

Aqui está outra implementação de PoC:

  • sem bibliotecas (por exemplo, lodash) para rebater
  • usando a API React Hooks

Espero que ajude :)

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
fonte
4

Há um use-debouncepacote que você pode usar com os ganchos do ReactJS.

No README do pacote:

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 você pode ver no exemplo acima, ele está configurado para atualizar a variável valueapenas uma vez a cada segundo (1000 milissegundos).

Arte
fonte
3

Apenas mais uma variante com recente reagir 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)} />)
  }
}
puchu
fonte
3

Uma solução agradável e limpa, que não requer nenhuma dependência externa:

Debouncing com ganchos de reação

Ele usa um personalizado mais os ganchos useEffect React e o método setTimeout/ clearTimeout.

Bruno Silvano
fonte
3

Você tentou?

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
fonte
2

Em vez de agrupar o handleOnChange em um debounce (), por que não agrupar a chamada ajax na função de retorno de chamada dentro do debounce, não destruindo o objeto de evento. Então, algo como isto:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}
Robert
fonte
4
Como o objeto de evento não é imutável e é destruído pelo ReactJS, mesmo que você envolva e obtenha uma captura de fechamento, o código falhará.
Henrik
2

Aqui está um exemplo que eu inventei que envolve outra classe com um debouncer. Isso se presta muito bem a ser transformado em uma função 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 <Thingy {...restOfProps} {...this.state} />
    }
}
mlucool
fonte
2

Agora existe outra solução para o React e o React Native no final de 2019 :

react-debounce-component

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

É um componente, fácil de usar, pequeno e widley suportado

Exemplo:

insira a descrição da imagem aqui

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;

* Eu sou o criador deste componente

Rebs
fonte
1

Eu estava procurando uma solução para o mesmo problema e me deparei com esse segmento, além de outros, mas eles tinham o mesmo problema: se você está tentando handleOnChangeexecutar uma função e precisa do valor de um destino de evento, receberá cannot read property value of nullou tal erro. No meu caso, eu também precisei preservar o contexto da thisfunção debounce, pois estou executando uma ação fluível. Aqui está a minha solução, ela funciona bem para o meu caso de uso, então eu a deixo aqui caso alguém se depare com esse segmento:

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

para throttleou debouncea melhor maneira é criar uma função de criador de modo que você pode 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 rendermétodo você pode fazer:

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

a updateUserProfileField método criará uma função separada sempre que você a chamar.

Nota: não tente retornar 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 função do acelerador cada vez que o evento for chamado, em vez de usar a mesma função do acelerador, portanto, basicamente, o acelerador será inútil;)

Além disso, se você usa debounceou throttlenão precisa setTimeoutou clearTimeout, é por isso que nós os usamos: P

Alnamrouti com tarifa
fonte
1

Aqui está um trecho de código usando a abordagem do @ Abra envolvida em um componente de função (usamos tecido para a interface do usuário, basta substituí-lo por um botão simples)

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}
        />
    );
}
Thread Pitt
fonte
1

Minha solução é baseada em ganchos (escritos em Typescript).

Eu tenho dois ganchos principais useDebouncedValueeuseDebouncedCallback

Primeiro - useDebouncedValue

Digamos que temos uma caixa de pesquisa, mas queremos solicitar ao servidor os resultados da pesquisa depois que o usuário parar de digitar 0,5s

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

Implementação

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

Ele apenas cria uma função 'debounce' no escopo do seu componente.

Digamos que temos um componente com um botão que exibirá o alerta 500ms depois que você parou de clicar nele.

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

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

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

Implementação (note que estou usando lodash / debounce como um auxiliar)

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
fonte
0

Aqui está um exemplo de TypeScript funcional para quem usa o TS e deseja renunciar a asyncfunções.

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
fonte
0

um pouco tarde aqui, mas isso deve ajudar. crie essa classe (está escrita em texto datilografado, mas é fácil convertê-la em 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 usar

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

você pode 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
fonte
0

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

tl; dr version : quando você atualizaria para os observadores, envie um método de agendamento para chamar e, por sua vez, notificará os observadores (ou executará ajax, etc)

Jsfiddle completo com o componente de exemplo 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
fonte
3
Isso não tornará o estado / momento da rejeição global em todas as instâncias do InputField, porque ele foi criado com a definição de classe? Talvez seja isso que você deseja, mas vale a pena notar, independentemente.
robbles
1
perigoso se for montado várias vezes no dom, verifique stackoverflow.com/questions/23123138/… #
911 Sebastien Lorber
2
Esta é uma solução ruim, devido a problemas de montagem dupla - você está fazendo sua função para agendarAlterar um singleton e isso não é uma boa ideia. -1
Henrik
0

Evite usar event.persist()- você deseja permitir que o React recicle o evento sintético. Eu acho que a maneira mais limpa de usar classes ou ganchos é dividir o retorno de chamada em duas partes:

  1. O retorno de chamada sem rebaixamento
  2. Chama uma função rejeitada com apenas as partes do evento que você precisa (para que o evento sintético possa ser reciclado)

Aulas

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

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

<div onMouseOver={this.onMouseOver} />

Funções

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

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

<div onMouseOver={this.onMouseOver} />

Observe que, se sua handleMouseOverfunção usa o estado de dentro do componente, você deve usá-lo em useMemovez de useRefe passá-lo como dependências, caso contrário você estará trabalhando com dados obsoletos (não se aplica às classes, é claro).

Dominic
fonte
0

Gancho de extensão

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;

Use 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.
fonte
0

Conheci esse problema hoje. Resolvido usando setTimeoute clearTimeout.

Vou dar um exemplo que você pode 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

Você também pode refatorá-lo em seu próprio componente de função:

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 use-o 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
fonte