Artigo original: How to Use Debounce and Throttle in React and Abstract them into Hooks
Hooks (texto em inglês) são um adendo brilhante ao React. Eles simplificam muito da lógica que antes precisava ser dividida em ciclos de vida diferentes com componentes de classe
.
Eles, no entanto, exigem um modelo mental diferente, especialmente de quem está iniciando (texto em inglês).
Eu já gravei uma série de vídeos curtos a respeito do assunto deste artigo que você pode achar útil.
Debounce e throttle
Existem várias publicações dos blogs escritas sobre o debounce e o throttle, por isso, não vou entrar em detalhes sobre como escrever seu próprio debounce e throttle. Para sermos mais breves, considere o debounce
e o throttle
do Lodash.
Caso precise de uma revisão breve, os dois aceitam uma função (de callback) e um atraso (em inglês, delay) em milissegundos (digamos, x
) e, em seguida, retornam outra função com algum comportamento especial:
debounce
: retorna uma função que pode ser chamada quantas vezes quisermos (possivelmente em sucessões rápidas), mas somente chamará a função de callback após aguardar porx
ms desde a última chamada.throttle
: retorna uma função que pode ser chamada quantas vezes quisermos (possivelmente em sucessões rápidas), mas somente chamará a função de callback, no máximo, uma vez a cadax
ms.
Caso de uso
Temos um editor de blogs minimalista (este é o repositório dele no GitHub) e gostaríamos de salvar a publicação do blog no banco de dados 1 segundo após o usuário parar de digitar.
Você também pode consultar este Codesandbox se quiser ver a versão final do código.
Uma versão mínima do nosso editor tem esta aparência:
import React, { useState } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // would be an API call normally
const handleChange = event => {
setValue(event.target.value);
};
return (
<main>
<h1>Blog</h1>
<textarea value={value} onChange={handleChange} rows={5} cols={50} />
<section className="panels">
<div>
<h2>Editor (Client)</h2>
{value}
</div>
<div>
<h2>Saved (DB)</h2>
{dbValue}
</div>
</section>
</main>
);
}
Aqui, saveToDb
seria, de fato, uma chamada de API ao back-end. Para manter as coisas simples, estou salvando-a no state e renderizando-a como dbValue
.
Como queremos realizar essa operação de salvamento assim que o usuário parar de digitar (após 1 segundo), precisamos usar o debounce.
Aqui temos o repositório do código inicial e a branch.
Criação de uma função com debounce
Primeiro, precisamos de uma função com debounce que envolva a chamada a saveToDb
:
import React, { useState } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // normalmente, a chamada à API
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// Início do destaque
const debouncedSave = debounce(() => saveToDb(nextValue), 1000);
debouncedSave();
// Fim do destaque
};
return <main>{/* O mesmo que antes */}</main>;
}
Isso, no entanto, não funciona de fato, já que a função debouncedSave
é criada do zero a cada chamada de handleChange
. Ela acabaria fazendo o debouncing a cada toque de tecla em vez de fazer o debouncing do valor inteiro do input.
useCallback
useCallback
normalmente é utilizada para otimizações de desempenho ao passar callbacks para os componentes filhos. Porém, podemos usar sua restrição de memoizar uma função de callback para garantir que debouncedSave
faça referência à mesma função com debounce por todas as renderizações.
Eu também escrevi este artigo (texto em inglês) aqui no freeCodeCamp caso você deseje entender as noções básicas de memoização.
Desta vez, funcionará conforme o esperado:
import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // normalmente, a chamada à API
// Início do destaque
const debouncedSave = useCallback(
debounce(nextValue => saveToDb(nextValue), 1000),
[], // será criada apenas uma vez inicialmente
);
// Fim do destaque
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// Mesmo que handleChange seja criada a cada renderização e executada
// ela fará referência à mesma debouncedSave criada inicialmente
debouncedSave(nextValue);
};
return <main>{/* O mesmo que antes */}</main>;
}
useRef
useRef
nos dá um objeto mutável, cuja propriedade current
faz referência ao valor inicial passado. Se não o alterarmos manualmente, o valor persistirá por todo o tempo de vida do componente.
Isso é semelhante às propriedades de instâncias de uma classe (ou seja, definir métodos e propriedades com this
).
Isto também funcionará conforme o esperado:
import React, { useState, useRef } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // normalmente, a chamada à API
// Isso permanece o mesmo em todas as renderizações
// Início do destaque
const debouncedSave = useRef(debounce(nextValue => saveToDb(nextValue), 1000))
.current;
// Fim do destaque
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// Mesmo que handleChange seja criada a cada renderização e executada
// ela fará referência à mesma debouncedSave criada inicialmente
debouncedSave(nextValue);
};
return <main>{/* O mesmo que antes */}</main>;
}
Continue sua leitura pelo blog do autor para saber como abstrair esses conceitos em hooks personalizados ou confira a série de vídeos.
Você também pode seguir o autor no Twitter para ficar atualizado quanto aos seus artigos mais recentes. Espero que você tenha achado este artigo útil. :)