Artigo original escrito por: Divyanshu Maithani
Artigo original: How to Use Debounce and Throttle in React and Abstract them into Hooks
Traduzido e adaptado por: Daniel Rosa

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 por x 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 cada x 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. :)