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 _.debounce
sublinhado 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, this
não é o próprio objeto criado. this.method
não retorna o que você espera, porque o this
contexto 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 debouncedMethod
chamada, 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 SyntheticEvent
propriedades 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=true
porquepersist
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.
debounce
. aqui, quandoonChange={debounce(this.handleOnChange, 200)}/>
, ele será invocadodebounce function
sempre. mas, de fato, o que precisamos é chamar a função que função de retorno retornou.