Diminuindo Processamento com useMemo em React
Cada vez mais nossos usuários exigem que os sistemas sejam mais rápidos e infelizmente algumas vezes deixamos passar pequenos detalhes que podem fazer toda diferença em ambientes de produção. Podemos adicionar toda regra para realizar lazy loading, code splitting, cache, aplicar técnias de performance, etc…
Mas, um detalhe muito importante é a quantidade de processamento que nossos componentes estão realizando, será que eles só processam aquilo que é necessário? Para exemplificar o problema, vamos começar criando um novo projeto em React:
create-react-app post-utilizando-use-memo
Obs: Sim, eu ainda prefiro utilizar o CRA instalado localmente na minha máquina.
Aguarde todo processo de download e configuração finalizar.
Uma vez que tudo terminou, podemos acessar nosso projeto:
cd post-utilizando-use-memo
E subir o servidor de desenvolvimento:
npm start
Com isso temos uma aplicação padrão do React rodando:
Vamos adicionar algumas funcionalidades:
- Implementar um contador que poderá ser decrementado ou incrementado.
- Implementar um botão para gerar um número aleatório e adicioná-lo em uma lista.
Chega de papo e vamos para os códigos.
Implementar um contador que poderá ser decrementado ou incrementado
Nesse momento não irei focar muito no React em si, então não irei passar por todos os passos explicando cada um, basicamente vamos abrir o App.js
e realizar algumas modificações nos códigos do mesmo, o resultado será:
import React, { useState } from 'react';
import './App.css';
function App() {
const [counter, updateCounter] = useState(0)
const handleDecrement = () => updateCounter(counter - 1)
const handleIncrement = () => updateCounter(counter + 1)
return (
<div className="App">
<fieldset>
<legend>Counter</legend>
<p>Contador: {counter}</p>
<button onClick={handleDecrement}>Decrementar</button>
<button onClick={handleIncrement}>Incrementar</button>
</fieldset>
</div>
);
}
export default App;
Após salvar as modificações, podemos voltar no navegador e ver que a interface do contador está pronta:
Isso deve ser o suficiente para nosso contador estar funcionando com suas duas opções (decrementar e incrementar), podemos testá-lo e ver que tudo funciona como o esperado:
Com a primeira funcionalidade pronta, vamos implementar a segunda.
Implementar um botão para gerar um número aleatório e adicioná-lo em uma lista
Assim como foi feito com o contador, não irei passar por todo o processo de implementação do número aleatório e sim disponibilizar o código final do App.js
:
import React, { useState } from 'react';
import './App.css';
function App() {
const [counter, updateCounter] = useState(0)
const [numbers, updateNumbers] = useState([])
const handleDecrement = () => updateCounter(counter - 1)
const handleIncrement = () => updateCounter(counter + 1)
const handleAdd = () => updateNumbers([
...numbers,
Math.random().toFixed(2),
])
return (
<div className="App">
<fieldset>
<legend>Counter</legend>
<p>Contador: {counter}</p>
<button onClick={handleDecrement}>Decrementar</button>
<button onClick={handleIncrement}>Incrementar</button>
</fieldset>
<fieldset>
<legend>Números</legend>
<ul>
{numbers.map((n, i) => <li key={i}>{n}</li>)}
</ul>
<button onClick={handleAdd}>Adicionar</button>
</fieldset>
</div>
);
}
export default App;
Com essas modificações feitas, devemos ter nossa listagem de números prontas:
E funcionando:
Maravilha, tudo funcionando como o esperado.
Visualizando o problema
Agora, vamos adicionar duas novas funcionalidades, queremos mostrar o contador com o valor dobrado e multiplicado por si, ou seja:
Caso o valor do contador seja 3, o valor dobrado será 2x3 = 6 e o valor multiplicado por si será 3x3 = 9
Como de costume, vamos modificar nosso App.js
:
import React, { useState } from 'react';
import './App.css';
function App() {
const [counter, updateCounter] = useState(0)
const [numbers, updateNumbers] = useState([])
const counterDouble = counter * 2
const counterMult = counter * counter
const handleDecrement = () => updateCounter(counter - 1)
const handleIncrement = () => updateCounter(counter + 1)
const handleAdd = () => updateNumbers([
...numbers,
Math.random().toFixed(2),
])
return (
<div className="App">
<fieldset>
<legend>Counter</legend>
<p>Contador: {counter}</p>
<p>Contador dobrado: {counterDouble}</p>
<p>Contador multiplicado: {counterMult}</p>
<button onClick={handleDecrement}>Decrementar</button>
<button onClick={handleIncrement}>Incrementar</button>
</fieldset>
<fieldset>
<legend>Números</legend>
<ul>
{numbers.map((n, i) => <li key={i}>{n}</li>)}
</ul>
<button onClick={handleAdd}>Adicionar</button>
</fieldset>
</div>
);
}
export default App;
Após realizar as devidas modificações e testes, podemos ver que tudo continua funcionando como o esperado. O problema é que nosso counterDouble
e counterMult
sempre estão sendo processados, mesmo que o valor do counter
não mude, as multiplicações estão sendo processadas.
Para que esse problema fique mais claro, vamos adicionar um novo contador que será multiplicado por um valor randômico:
import React, { useState } from 'react';
import './App.css';
function App() {
const [counter, updateCounter] = useState(0)
const [numbers, updateNumbers] = useState([])
const counterDouble = counter * 2
const counterMult = counter * counter
const counterRand = counter * Math.random()
const handleDecrement = () => updateCounter(counter - 1)
const handleIncrement = () => updateCounter(counter + 1)
const handleAdd = () => updateNumbers([
...numbers,
Math.random().toFixed(2),
])
return (
<div className="App">
<fieldset>
<legend>Counter</legend>
<p>Contador: {counter}</p>
<p>Contador dobrado: {counterDouble}</p>
<p>Contador multiplicado: {counterMult}</p>
<p>Contador randômicro: {counterRand}</p>
<button onClick={handleDecrement}>Decrementar</button>
<button onClick={handleIncrement}>Incrementar</button>
</fieldset>
<fieldset>
<legend>Números</legend>
<ul>
{numbers.map((n, i) => <li key={i}>{n}</li>)}
</ul>
<button onClick={handleAdd}>Adicionar</button>
</fieldset>
</div>
);
}
export default App;
Agora para testar, vamos incrementar o contador algumas vezes e depois adicionar alguns números para a listagem:
Repare que ao adicionar novos números na lista, o contador randômico também é atualizado, porém, o valor do contador não mudou, então esse processamento não deveria ser feito.
Cacheando processamento com useMemo
Para essas situações foi criado o hook chamado useMemo
, com ele conseguimos garantir que o processamento das contas somente será realizado caso o valor do counter
mude.
Para usá-lo devemos passar uma função como primeiro parâmetro, o retorno da função será o valor armazenado em nossa variável e como segundo parâmetro informamos um array
, onde cada item do array
será utilizado para verificar se o processamento deve ou não ser feito, por exemplo:
const counterDouble = useMemo(() => counter * 2, [counter])
Nesse trecho estamos passando uma arrow function como primeiro parâmetro, ela irá multiplicar o valor do counter
por 2
e logo em seguida retornar o resultado da multiplicação. Sendo assim, o resultado será armazenado na variável counterDouble
.
Como segundo parâmetro, estamos passando um array
com o state counter
, isso porque ele é a variável que queremos utilizar como base para a verificação do processamento ou não, ou seja, caso o valor do counter
mude o processamento deve ser feito, senão, o valor deve ser retornado da memória.
Essa pratica de memorizar um valor para economisar processamento é conhecida como memoized, por isso o hook chama useMemo
(Memo
de memoized). Caso queira saber mais sobre o assunto, recentemente postei um artigo no blog sobre o mesmo:
Por fim, vamos refatorar nosso App.js
para fazer uso do useMemo
em nossas variáveis computadas:
import React, { useMemo, useState } from 'react';
import './App.css';
function App() {
const [counter, updateCounter] = useState(0)
const [numbers, updateNumbers] = useState([])
const counterDouble = useMemo(() => counter * 2, [counter])
const counterMult = useMemo(() => counter * counter, [counter])
const counterRand = useMemo(() => counter * Math.random(), [counter])
const handleDecrement = () => updateCounter(counter - 1)
const handleIncrement = () => updateCounter(counter + 1)
const handleAdd = () => updateNumbers([
...numbers,
Math.random().toFixed(2),
])
return (
<div className="App">
<fieldset>
<legend>Counter</legend>
<p>Contador: {counter}</p>
<p>Contador dobrado: {counterDouble}</p>
<p>Contador multiplicado: {counterMult}</p>
<p>Contador randômicro: {counterRand}</p>
<button onClick={handleDecrement}>Decrementar</button>
<button onClick={handleIncrement}>Incrementar</button>
</fieldset>
<fieldset>
<legend>Números</legend>
<ul>
{numbers.map((n, i) => <li key={i}>{n}</li>)}
</ul>
<button onClick={handleAdd}>Adicionar</button>
</fieldset>
</div>
);
}
export default App;
Com essas modificações, podemos realizar o teste novamente, ou seja, incrementar o contador algumas vezes e depois adicionar alguns números para a listagem:
Repare que agora ao adicionar novos itens na listagem, os valores do contador não mudam, isso porque o valor do contador não mudou, sendo assim, não é necessário processá-lo novamente e o valor é pego da memória.
Conclusão
Nesse post vimos como podemos utilizar o hook useMemo
para memorizar resultados e economisar no processamento de variáveis.
Se você gostou do post e quer receber novidades por email, fique à vontade para assinar a newsletter abaixo.
Abraços, até a próxima.