Melhorando a performance das nossas aplicações em ReactJS

Melhorando a performance das nossas aplicações em ReactJS

Durante a sua jornada como desenvolvedor ReactJS, você sempre deve pensar na performance das suas aplicações, principalmente quando você faz o uso de hooks como useState.

É imprecindível que você desenvolva um olhar mais crítico nesse aspecto, isto para que suas aplicações sejam rápidas e não fiquem consumindo memória de forma indesejada (memory leak).

Nesta lição eu vou te ensinar um pouco mais sobre o funcionamento das re-renderizações que podem acontecer em nossas aplicações, e como contornar cada uma delas.

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de otimizando-componentes:  

npx create-react-app otimizando-componentes

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

Em seguida vamos criar o nosso primeiro componente chamado de MeuComponente:

MeuComponente > index.jsx:

const MeuComponente = () => {
 return(
 <>
 <h1>Meu Componente</h1>
 </>
 )
}

export default MeuComponente;

  Não se esqueça também de inserir o seu componente dentro do App.js:  

import MeuComponente from "./components/MeuComponente";

export default function App() {
 return(
 <div>
 <MeuComponente />
 </div>
 )
}

Done? Então partiu otimizar nossos componentes 😉

Identificando Renderizações Repetidas

Em lições anteriores, o termo re-renderizações foi visto durante alguns vezes quando ainda estávamos discutindo alguns tópicos.

Apesar de termos mencionado, creio que você não deve ter compreendido esse termo em sua totalidade, até porque nós não entramos nesse assunto ainda 😅

Você sabia que o ReactJS tem um péssimo costume de re-montar (re-renderizar) componentes mesmo quando não é estritamente necessário?

Isso é uma questão um pouco preocupante, especialmente em termos de desempenho, e isso tende a piorar a medida em que nossas aplicações ficam cada vez maiores e mais complexas.

É como se você estivesse construindo a mesma casa várias vezes só para mudar uma cor de tinta na parede 🥲

Vamos ver isso na prática? Vamos começar criando um novo componente chamado de Cabecalho.

Cabecalho > index.jsx:

let quantidadeDeRenderizacoes = 0;

const Cabecalho = () => {
 quantidadeDeRenderizacoes++;
 
 return (
 <h3>O Componente Cabeçalho Renderizou: {quantidadeDeRenderizacoes}</h3>
 )
}

export default Cabecalho;

Não esqueça de inserí-lo dentro do MeuComponente, que aliás também conta com algumas modificações a mais:

import { useState } from "react";
import Cabecalho from "../Cabecalho";

const MeuComponente = () => {
 const [nome, setNome] = useState("");
 return(
 <>
 <Cabecalho />
 <h1>Meu Componente</h1>
 <input type="text" placeholder="Nome" value={nome} onChange={(e) => setNome(e.target.value) } />
 </>
 )
}
 
export default MeuComponente;

Se você executar o projeto (npm start) e digitar qualquer coisa dentro daquele input, você verá que o Cabecalho está sendo montado toda vez que digitamos algo, veja:

Observe que a cada digito que fazemos, uma nova re-renderização do componente Cabecalho é realizada, mesmo que nenhuma alteração esteja sendo realizada dentro dele.

Sendo assim, qualquer nova função, efeito, implementação existente dentro do MeuComponente, ocasionaria uma remontagem do componente Cabecalho.

E é o que acontece quando você digita algo no <input> que existe dentro do MeuComponente, tenha em mente que a remontagem do componente Cabecalho ainda aconteceria caso usássemos outros tipos de formulários, como: <input>, <textarea> e <select> e etc.

Mas será que do jeito que está sendo feito, a nossa aplicação está errada?

Não enquanto a nossa aplicação contar com apenas poucos formulários 😉

Pois a partir do momento em que a aplicação conta com uma quantidade absurda de formulários, isso pode afetar a performance da nossa aplicação como um todo.

Perceba que o componente Cabecalho foi renderizado 70 vezes, sendo que não chegamos a realizar nenhuma modificação dentro dele, muito menos passar alguma props.

Isso aconteceu devido ao estado do componente MeuComponente ser atualizado de acordo com as mudanças que ocorreram dentro input, o que fez com que o componente MeuComponente fosse renderizado novamente.

Como o componente Cabecalho é filho direto de MeuComponente, ele também é re-renderizado, mesmo que não haja nenhuma mudança direta nos seus próprios props ou estados internos.

E essa é uma das característica do ReactJS. Fazendo com que quando um componente pai é renderizado, todos os seus componentes filhos também são renderizados, independentemente de terem ou não sofrido mudanças em seus props ou estados.

Nesse caso, o ReactJS não faz a diferenciação entre os componentes nesse nível quando se trata de questão relacionadas a renderização 🤯

Mesmo que o input não tenha relação direta com o componente Cabecalho, qualquer mudança que provoque a renderização do componente pai, fará com que todos os seus componentes filhos sejam renderizados novamente.

Sendo assim, existem algumas formas e maneiras diferentes de se resolver essa situação, veremos cada uma delas a seguir 😉

useMemo

O useMemo é um hook do ReactJS que permite memorizar o resultado de uma função de forma que ela seja reavaliada apenas quando as dependências especificadas mudarem.

Isso pode ser útil para otimizar o desempenho de componentes, evitando cálculos computacionais desnecessários.

Para usar o useMemo você deve importá-lo no seu projeto da seguinte forma:

import React, { useMemo } from "react";

Caso desejar importar outras funcionalidades de hooks, basta fazer isso usando a vírgula:

import React, { useState, useEffect, useMemo } from "react";

A função useMemo recebe dois argumentos principais, o primeiro deles refere-se a uma função, e o segundo um array de dependências.

A função especificada é chamada pelo useMemo, que retorna o resultado dessa função.

Já o array de dependências é opcional e serve para indicar quais variáveis devem ser monitoradas.

Se alguma dessas variáveis mudarem entre renderizações, a função será chamada novamente, caso contrário, o resultado que já foi memorizado será retornado sem a necessidade de um recálculo (re-renderização ou uma remontagem).

Para exemplificar, vamos criar um novo componente chamado de ReCalculo.

ReCalculo > index.jsx:

import React, {useState} from "react";

const ReCalculo = () => {
 const [nome, setNome] = useState("");

 const rank = () => {
 console.log("Calculando Rank... 10");
 return 10;
 }

 return(
 <>
 <input type="text" placeholder="Qual seu Nome?" value={nome} onChange={(e) => setNome(e.target.value) } />
 <p>Meu nome é {nome}, e sou rank {rank()}</p>
 </>
 )
}

export default ReCalculo;

Perceba que a cada vez em que digitamos algo no nosso <input>, a função rank() é chamada diversas vezes:

Isso aconteceu, pois como dito anteriormente, o ReactJS fica remontando o componente na tela toda vez que um estado é mudado. E como temos a função rank() sendo chamada dentro do nosso return:

<p>Meu nome é {nome}, e sou rank {rank()}</p>

É bem obvio que o ReactJS vai ficar chamando a função a cada vez que o componente é remontado, não é verdade?

Beleza, e como resolvemos isso?

Por meio da função useMemo da seguinte forma:

import React, { useState, useMemo } from "react";

const ReCalculo = () => {
 const [nome, setNome] = useState("");

 const rank = useMemo(() => {
 console.log("Calculando Rank... 10");
 return 10;
 }, []); // Sem dependências, pois rank não depende de nenhuma variável

 return (
 <>
 <input type="text" placeholder="Qual seu Nome?" value={nome} onChange={(e) => setNome(e.target.value)} />
 <p>Meu nome é {nome}, e sou rank {rank}</p>
 </>
 );
}

export default ReCalculo;

Executando o projeto novamente, assim que digitarmos algo no nosso <input>, a função rank() será executada apenas uma única vez (2 vezes por causa do StrictMode rs).

Como não há dependências no momento, ela será montada apenas uma vez, mesmo que o estado da aplicação fique mudando, o que resulta em um comportamento mais eficiente e performático.

Beleza, mas e se a função rank tivesse relacionada com uma dependência?

Vamos supor que a função rank dependa do estado nome, ou seja, ela calcula o rank com base no nome inserido no input. Nesse caso, podemos passar nome como uma dependência para o useMemo. (da mesma forma com que fizemos no useEffect em lições passadas), Observe:

import React, { useState, useMemo } from "react";

const ReCalculo = () => {
 const [nome, setNome] = useState("");

 const rank = useMemo(() => {
 console.log(`Calculando Rank para ${nome}...`);
 // Simulação de cálculo de rank com base no nome
 return nome.length * 2; // Rank calculado com base no comprimento do nome
 }, [nome]); // Dependência: nome

 return (
 <>
 <input type="text" placeholder="Qual seu Nome?" value={nome} onChange={(e) => setNome(e.target.value)} />
 <p>Meu nome é {nome}, e sou rank {rank}</p>
 </>
 );
}

export default ReCalculo;

No código acima o <input> ainda vai chamar a função rank() pois definimos a variável nome ela como dependência do useMemo(), e como o estado nome fica mudando a cada digito no <input> a useMemo() será chamada toda hora.

Agora, vamos supor que a função rank() dependa exclusivamente de um outro estado chamado de level:

import React, { useState, useMemo } from "react";

const ReCalculo = () => {
 const [nome, setNome] = useState("");
 const [level, setLevel] = useState(1);

 const rank = useMemo(() => {
 console.log(`Calculando Rank no nível ${level}...`);
 // Simulação de cálculo de rank com base no nível
 return level * 10; // Exemplo simples: o rank é 10 vezes o nível
 }, [level]); // Dependência apenas do level

 return (
 <>
 <input type="text" placeholder="Qual seu Nome?" value={nome} onChange={(e) => setNome(e.target.value)} />
 <input type="number" placeholder="Nível" value={level} onChange={(e) => setLevel(Number(e.target.value))} />
 <p>Meu nome é {nome}, e sou rank {rank}</p>
 </>
 );
}

export default ReCalculo;

No código acima, podemos mudar o estado nome quantas vezes quisermos que somente tal estado será alterado sem executar o rank(). Já o rank() só será chamado apenas quando mudarmos o level do usuário, ou seja, digitar algum valor no segundo <input>.

React.memo

O React.memo é uma função de ordem superior (HOC) no ReactJS que memoriza a renderização de componentes funcionais, impedindo que sejam re-renderizados caso suas props não mudarem. 

Isso pode melhorar significativamente o desempenho de uma aplicação ReactJS, especialmente quando há componentes funcionais que são renderizados com frequência, mas cujas props raramente mudam.

O que é ideal para corrigirmos aquele problema que aconteceu dentro do nosso componente Cabecalho 😉

Para testarmos o React.memo, vamos criar um novo componente chamado de Memorizado.

Memorizado > index.jsx:

import React from 'react';

const Memorizado = React.memo((props) => {
 const nome = () => {
 console.log('O Nome do componente é Memorizado!');
 return 'Memorizado';
 }
 return <div>O {nome()} renderizou com prop: {props.prop}</div>;
});

export default Memorizado;

A principio é um componente bem simples que conta com uma única mensagem. A diferença é que estamos envolvendo toda a sua execução dentro do React.memo.

Vamos importar este componente dentro do MeuComponente que criamos anteriormente:

import { useState } from "react";
import Cabecalho from "../Cabecalho";
import Memorizado from "../Memorizado";

const MeuComponente = () => {
 const [nome, setNome] = useState("");
 return(
 <>
 <Cabecalho />
 <Memorizado prop="ABCD" />
 <h1>Meu Componente</h1>
 <input type="text" placeholder="Nome" value={nome} onChange={(e) => setNome(e.target.value) } />
 </>
 )
}
 
export default MeuComponente;

Se você digitar algo no <input> verás que a renderização do componente não muda e que a mensagem "O Nome do componente é Memorizado!" é executada apenas uma vez.

Agora, experimente remover o React.memo do componente Memorizado:

import React from 'react';

const Memorizado = (props) => {
 const nome = () => {
 console.log('O Nome do componente é Memorizado!');
 return 'Memorizado';
 }
 return <div>O {nome()} renderizou com prop: {props.prop}</div>;
};

export default Memorizado;

E veja quantas vezes aquela mensagem aparece no console 😅

Com o React.memo conseguimos memorizar todo um componente evitando renderizações desnecessárias.

Agora voltando ao nosso componente Cabecalho, se você deseja evitar que o Cabecalho seja remontado em cada mudança no <input>, você pode envolvê-lo usando o React.memo, que memorizará o componente, e previnirá a sua renderização desnecessária caso suas props não tenham sido alteradas, o que evita uma segunda renderização:

import React from 'react';

const Cabecalho = () => {
 return (
 <h3>O Componente Cabeçalho Renderizou</h3>
 );
}

export default React.memo(Cabecalho);

Observe que nos exemplos deste tópico usamos o React.memo de formas diferentes, a primeira estava envolvendo a função anônima, e a segunda envolvendo só retorno no export default.

React.memo VS useMemo

Existem diferenças fundamentais entre o React.memo e useMemo, vejamos cada uma delas abaixo.

Em questão da finalidade, nós temos:

React.memo: É um Higher-Order Component (HOC) usado para memorizar a renderização de componentes funcionais. Ele impede que um componente funcional seja renderizado novamente se suas props não mudaram.

useMemo: É um hook usado para memorizar o resultado de uma função. Ele memoriza o valor de retorno de uma função, reavaliando-a apenas quando as dependências fornecidas mudam.

Já em questão da aplicação, nós temos:

React.memo é aplicado a componentes funcionais para memorizá-los.

useMemo é aplicado dentro de componentes funcionais para memorizar valores computados, como o resultado de uma função por exemplo.

Já no quesito de uso de dependências, nós temos:

React.memo não requer a especificação de dependências explicitamente. Ele memoriza o componente inteiro com base em suas props.

useMemo requer a especificação de dependências como o segundo argumento. Ele reavalia o valor memorizado apenas quando uma ou mais dessas dependências mudarem.

Em resumo, enquanto React.memo é usado para memorizar a renderização de componentes funcionais em relação às suas props, useMemo é usado para memorizar valores computados dentro de componentes funcionais, levando em consideração dependências específicas.

Reconciliation

O ReactJS possui um mecanismo chamado de "reconciliation", que é responsável por atualizar eficientemente a árvore de elementos do DOM. E você já viu o seu funcionamento em ação 😄

Em vez de reconstruir toda a árvore do zero a cada atualização, o ReactJS compara a árvore atual com a nova versão e faz as mínimas alterações necessárias para refletir a nova versão.

O reconciliation acontece quando executamos um loop e informamos o key em cada elemento HTML, por exemplo:

import React from 'react';

class ItemList extends React.Component {
 render() {
 return (
 <ul>
 {this.props.items.map(item => (
 <li key={item.id}>{item.name}</li>
 ))}
 </ul>
 );
 }
}

E como vimos em lições anteriores, o key é um atributo essêncial para nossa aplicação não adicionar novos elementos a cada mudança de estado.

Tal técnica é conhecida no mundo do ReactJS como reconciliation, e representa uma das formas de melhorar a performance das nossas aplicações usando o atributo key 😉

useCallback

O useCallback é um hook do ReactJS que é usado para otimizar o desempenho, especialmente em casos em que você precisa passar uma função como propriedade para componentes filhos.

Ele funciona de forma similar ao useMemo, só que em vez de retornar um valor único, ele retorna uma função.

Seu objetivo é retornar uma versão memorizada da função callback que só muda se uma das dependências passadas como segundo argumento mudar.

Isso é útil para evitar a recriação da função em cada renderização, o que pode levar a problemas de desempenho em toda a sua aplicação.

Para usar o useCallback basta importá-lo dentro do seu componente da seguinte forma:

import React, { useCallback } from 'react';

Se você quiser usar o useCallback em conjunto com outros hooks, você já sabe o que fazer 😉

import React, { useState, useCallback, useEffect } from 'react';

Para exemplificar o seu uso, vamos criar dois novos componentes, chamados de Botao e CallBack.

Botao > index.jsx:

const Botao = ({ onClick }) => {
 console.log('Componente Botao renderizado!');
 return <button onClick={onClick}>Incremantar</button>;
}

export default Botao;

CallBack > index.jsx:

import React, { useState, useCallback } from 'react';
import Botao from '../Botao';

const CallBack = () => {

 const [contador, setContador] = useState(0);

 const handleClick = useCallback(() => {
 setContador(contador + 1);
 }, [contador]); // A função só será recriada quando count mudar
 
 return (
 <div>
 <Botao onClick={handleClick} />
 <p>Contagem: {contador}</p>
 </div>
 );
}

export default CallBack;

No código acima estamos passando uma função para dentro do componente Botao, na tentativa de evitar uma re-renderizações desnecessária.

Agora vamos observer o mesmo exemplo só que sem o uso do useCallback:

import React, { useState } from 'react';
import Botao from '../Botao';

const CallBack = () => {
 const [contador, setContador] = useState(0);

 // Aqui, a função handleClick é recriada em cada renderização de App
 // Isso pode causar problemas de desempenho, especialmente em componentes filhos.

 const handleClick = () => {
 setContador(contador + 1);
 };

 return (
 <div>
 <Botao onClick={handleClick} />
 <p>Contagem: {contador}</p>
 </div>
 );
}

export default CallBack;

Sem o useCallback, a nossa função handleClick é recriada a cada renderização do nosso componente CallBack.

Isso significa que sempre que o estado contador mudar, uma nova função será criada. Isso pode não ser um problema em aplicações pequenas, mas em aplicações maiores ou quando esta função é passada como prop para muitos componentes, pode ocasionar um impacto negativo no desempenho da nossa aplicaçõa.

Vamos analisar um outro exemplo de uso em que o hook useCallback se faz extremamente necessário. Para isso criei um novo componente chamado Alocado.

Allocado > index.jsx:

import React, { useState } from 'react';

const Alocado = () => {

 const [tarefas, setTarefas] = useState([
 'Estudar ReactJS',
 'Comprar uma casa de 500m²'
 ])

 const [nome, setNome] = useState('')

 const [idade, setIdade] = useState(29)

 const [input, setInput] = useState('');


 function mudaNome(){
 setNome('Micilini');
 setIdade(30);
 setTarefas([...tarefas, 'Ir dormir!']);
 }

 return(
 <div>
 <h1>Alocado!</h1>
 <ul>
 {tarefas.map(tarefa => (
 <li key={tarefa}>{tarefa}</li>
 ))}
 </ul>
 <p>Meu Nome é: {nome}</p>
 <button type="button" onClick={mudaNome}>Muda Nome</button>

 <input type="text" value={input} onChange={(e) => setInput(e.target.value)} />
 </div>
 );
}

export default Alocado;

Sempre quando executamos a função mudaNome(), por de baixo dos panos, nossa função é alocada e desalocada da memoria toda vez que ela é chamada, isso acontece pois ela existe dentro do componente Alocado.

O que representa um gasto a mais na memoria da nossa aplicação.

A razão pela qual a função é recriada a cada renderização é que ela é definida dentro do componente Alocado, e toda vez que o componente é renderizado, uma nova instância da função é criada. Isso pode causar uma pequena sobrecarga de desempenho, especialmente se o componente for renderizado frequentemente.

Para evitar essa recriação desnecessária da função, você pode usar o useCallback, que memoriza a função e a retorna apenas quando suas dependências mudarem.

Se você fizer isso, o desempenho da sua aplicação será melhorada, mesmo em situações onde a função é passada como propriedade para componentes filhos ou é utilizada como dependência em hooks. Observe:

import React, { useState, useCallback } from 'react';

const Alocado = () => {
 const [tarefas, setTarefas] = useState([
 'Estudar ReactJS',
 'Comprar uma casa de 500m²'
 ]);
 const [nome, setNome] = useState('');
 const [idade, setIdade] = useState(29);
 const [input, setInput] = useState('');

 const mudaNome = useCallback(() => {
 setNome('Micilini');
 setIdade(30);
 setTarefas([...tarefas, 'Ir dormir!']);
 }, [tarefas]);

 return(
 <div>
 <h1>Alocado!</h1>
 <ul>
 {tarefas.map(tarefa => (
 <li key={tarefa}>{tarefa}</li>
 ))}
 </ul>
 <p>Meu Nome é: {nome}</p>
 <button type="button" onClick={mudaNome}>Muda Nome</button>
 <input type="text" value={input} onChange={(e) => setInput(e.target.value)} />
 </div>
 );
}

export default Alocado;

Dessa forma, a função mudaNome será memorizada e recriada apenas quando a dependência [tarefas] mudar, evitando a recriação desnecessária a cada renderização do componente Alocado.

Isso significa que sempre devemos fazer o uso do useCallback em qualquer função que muda o seu estado?

Não necessariamente. O uso de useCallback é recomendado em certos casos para otimização de desempenho, mas não é uma regra absoluta dizer que todas as funções que atualizam o estado, devem ser envoltas em useCallback.

O useCallback é mais útil em situações onde a função é passada como propriedade para componentes filhos ou é utilizada como dependência em hooks, porque ele memoriza a função e a retorna apenas quando suas dependências mudam.

Isso pode ajudar a evitar re-renderizações desnecessárias de componentes filhos, especialmente em componentes grandes ou que são renderizados frequentemente.

useRef

Por fim, não menos importante, nós temos o hook useRef que também pode atuar como uma alternativa ao useState.

O useRef é um hook do React que permite criar uma referência mutável que pode ser persistida entre renderizações de um componente. Ao contrário do estado, as alterações em uma ref não causam uma nova renderização do componente em questão.

A principal utilização do useRef é para acessar ou modificar diretamente o DOM ou outros elementos que persistem durante todo o ciclo de vida do componente, tudio isso sem causar uma nova renderização.

Para importá-lo dentro do seu componente, basta usar:

import React, { useRef } from 'react';

Ou caso você queira importar mais de um hook, use vírgulas:

import React, { useRef, useState, useEffect } from 'react';

Para testar o uso do useRef, vamos criar um outro componente chamado de SegundoComponente que vai atuar de forma bem similar a primeira versão do MeuComponente.

SegundoComponente > index.jsx:

import { useState } from "react";
import Cabecalho from "../Cabecalho";

const SegundoComponente = () => {
 const [nome, setNome] = useState("");
 return(
 <>
 <Cabecalho />
 <h1>Segundo Componente</h1>
 <input type="text" placeholder="Nome" value={nome} onChange={(e) => setNome(e.target.value) } />
 </>
 )
}
 
export default SegundoComponente;

Vamos considerar também que o componente Cabecalho não possui suporte ao React.memo:

let quantidadeDeRenderizacoes = 0;

const Cabecalho = () => {
 quantidadeDeRenderizacoes++;
 
 return (
 <h3>O Componente Cabeçalho Renderizou: {quantidadeDeRenderizacoes}</h3>
 )
}

export default Cabecalho;

Se você notar, o SegundoComponente vai apresentar os mesmos problemas de re-renderizações desnecessárias no Cabecalho, isto é, quando o nosso <input> é preenchido.

Uma forma de corrigir isso, é fazendo o uso do useRef da seguinte forma:

import React, { useRef } from "react";
import Cabecalho from "../Cabecalho";

const SegundoComponente = () => {
 const nomeRef = useRef("");// Substitui o useState por useRef

 return(
 <>
 <Cabecalho />
 <h1>Segundo Componente</h1>
 <input type="text" placeholder="Nome" ref={nomeRef} />
 </>
 );
}
 
export default SegundoComponente;

Com o useRef precisamos passar um novo atributo chamado ref para dentro do nosso <input>, o que faz com que o nosso componente Cabecalho passe por renderizações desnecessárias.

Veja que em nenhuma vez o componente Cabecalho passou por uma renderização 😉

Se você quiser selecionar o valor do useRef que esta atrelado à aquele <input> após o usuário clicar no botão, você pode fazer isso da seguinte forma:

import React, { useRef } from "react";
import Cabecalho from "../Cabecalho";

const SegundoComponente = () => {
 const nomeRef = useRef("");

 const handleSubmit = () => {
 // Acessa o valor digitado no campo de input usando a propriedade current do objeto ref
 const nome = nomeRef.current.value;
 // Exibe um alerta com o valor digitado
 alert(`Nome digitado: ${nome}`);
 };

 return(
 <>
 <Cabecalho />
 <h1>Segundo Componente</h1>
 <input type="text" placeholder="Nome" ref={nomeRef} />
 {/* Botão que chama a função handleSubmit quando clicado */}
 <button onClick={handleSubmit}>Enviar</button>
 </>
 );
}
 
export default SegundoComponente;

Note que o valor armazenado dentro daquele <input> foi retornado acessando as chaves current.value.

Só que... existe alguns pequenos poréns ao usar o useRef:

Se você quiser por exemplo, atualizar o valor do nome assim que o usuário clica em um botão, você consegue:

import React, { useRef } from "react";
import Cabecalho from "../Cabecalho";

const SegundoComponente = () => {
 const nomeRef = useRef("");// Substitui o useState por useRef

 const atualizaNome = () => {
 nomeRef.current.value = "Micilini";
 }

 return(
 <>
 <Cabecalho />
 <h1>Segundo Componente</h1>
 <input type="text" placeholder="Nome" ref={nomeRef} />
 ?<button onClick={atualizaNome}>Atualizar Nome para "Micilini"</button>
 </>
 );
}
 
export default SegundoComponente;

Se você quiser atrelar o valor digitado pelo usuário na UI, você também não consegue:

import React, { useRef } from "react";
import Cabecalho from "../Cabecalho";

const SegundoComponente = () => {
 const nomeRef = useRef("");// Substitui o useState por useRef

 return(
 <>
 <Cabecalho />
 <h1>Segundo Componente: {nomeRef.current.value}</h1>
 <input type="text" placeholder="Nome" ref={nomeRef} />
 </>
 );
}
 
export default SegundoComponente;

Isso faz com que o useRef seja usado melhor em formulários simplistas, onde não precisamos carregar nenhuma informação necessária durante a execução da aplicação.

O que pode ser o caso de um <select> que depende do retorno de algum dado de uma API para preencher os itens na UI, em casos como esses, não tem jeito, só o useState consegue te atender.

Sendo assim, separei algumas ocasiões em que o useRef pode ser necessário:

Quando a sua aplicação não gera renderizações: Enquanto em muitos casos isso é uma vantagem (para evitar renderizações desnecessárias), pode ser uma desvantagem quando você precisa que mudanças em um valor causem uma re-renderização do componente. O useState, por outro lado, sempre causará uma re-renderização quando o estado é atualizado.

Quando a sua aplicação não gera reatividade: O useRef não é reativo, o que significa que não causa a re-execução de códigos que dependem de seu valor. Isso pode ser uma desvantagem em casos onde você deseja que uma atualização de estado cause automaticamente uma ação adicional.

Uso confuso em substituição do useState: Embora seja possível usar useRef para gerenciar o estado de um componente, ele não foi projetado para isso e pode levar a confusão no código, especialmente para desenvolvedores que estão acostumados com o padrão useState.

Quando sua aplicação tem um grande potencial para mutações diretas: Como useRef permite a mutação direta de valores, pode ser mais fácil introduzir bugs relacionados à mutabilidade do estado, especialmente em código complexo ou em equipes grandes.

Dificuldade em rastrear dependências: Em casos onde useRef é usado para armazenar valores que afetam o fluxo de renderização do componente, pode ser difícil rastrear todas as dependências desses valores, o que pode levar a comportamentos inesperados.

Então qual é a melhor forma de se otimizar os componentes da minha aplicação em ReactJS?

Nesta lição você analisou alguns casos de uso de hooks como useState, useCallback, React.memo, useMemo e o useRef.

Onde cada um deles executa procedimentos especificos na tentativa de melhorar a performance das nossas aplicações em ReactJS.

Se baseando no objetivo de cada um deles, se você quiser nota 10 no quesito de otimização e peformance das suas aplicações (mesmo que isso não seja totalmente necessário em alguns casos), você pode seguir as dicas abaixo 😉

Dica 1) Se você tem uma aplicação em que existem poucos estados, não é nenhum problema usar o useState para fazer o controle do ciclo de vida. Mas não se esqueça de fazer o uso do React.memo ou useMemo em seus componentes que precisam ser memorizados.

Dica 2) Caso sua aplicação possuir muitos estados, de modo que você precisa controlar diversos elementos de formulários (<input>, <textarea>, <select> e etc), o recomendável é que você faça o uso de uma biblioteca chamada de React-hook-Forms, o que excluí a utilização do useState (Veremos o funcionamento dessa biblioteca em lições futuras).

Dica 3) Caso você tiver muitos formulários onde estes não precisam ter seu estado controlado, o useRef acaba sendo uma melhor opção.

Dica 4) Se você for um fã das otimizações, você pode usar o useCallback em todas as funções de todos os seus componentes (pais e filhos). Apesar de que isso não é algo totalmente necessário rs

Dica 5) Se você possui um componente filho do tipo funcional que está sendo atrelado a um componente pai que trabalha com estados, é recomendável fazer o uso do React.memo para evitar renderizações desnecessárias do componente filho.

Dica 6) Ao lidar com listas grandes de elementos que precisam ser renderizados, utilize a chave key de forma eficiente. Certifique-se de que cada item na lista tenha uma chave única, preferencialmente um identificador único relacionado aos dados. Isso ajuda o ReactJS a identificar quais elementos foram modificados, adicionados ou removidos, otimizando o processo de renderização por meio do "reconciliation".

Dica 7) Se você perceber que determinadas partes de seus componentes são renderizadas com frequência, mesmo que seus dados permaneçam inalterados, considere usar a técnica de memorização com a função useMemo. Isso pode evitar cálculos repetitivos ou recriações de objetos em cada renderização.

Dica 8) Ao trabalhar com componentes que fazem solicitações assíncronas, como chamadas de API, é importante tratar adequadamente o ciclo de vida dessas solicitações. Utilize os métodos useEffect para gerenciar o início, a atualização e o término dessas solicitações, evitando vazamentos de memória ou problemas de renderização.

Dica 9) Para melhorar a legibilidade e a manutenção de seus componentes, divida-os em componentes menores e mais especializados. Isso não só torna o código mais modular e reutilizável, mas também facilita a identificação e a resolução de problemas específicos em cada parte do aplicativo.

Dica 10) Ao lidar com dados globais ou compartilhados entre vários componentes, considere o uso de contextos do ReactJS. (veremos mais sobre ele na próxima lição). Isso evita a necessidade de passar propriedades manualmente através de vários níveis de componentes e centraliza o gerenciamento de estado em um local acessível globalmente.

Identificando componentes que estão sendo re-renderizados

Existem algumas formas de identificar componentes que estão sendo re-montados (re-renderizados) na tela do usuario.

Atualmente existem um plugin muito bom chamado de React DevTools, que pode ser instalado no seu navegador do Google Chrome.

Nele, você pode inspecionar a hierarquia de componentes da sua aplicação ReactJS, visualizar o estado e as props de cada componente, bem como acompanhar as atualizações de renderização em tempo real.

Ou, caso você queria fazer algo um pouco mais manual, você pode apelar pelas estratégias que você viu nesta lição, de modo a executar mensagens no console e fazer contagens.

Por exemplo, você pode criar contadores que nem fizemos no componente Cabecalho:

let quantidadeDeRenderizacoes = 0;

const Cabecalho = () => {
 quantidadeDeRenderizacoes++;
 
 return (
 <h3>O Componente Cabeçalho Renderizou: {quantidadeDeRenderizacoes}</h3>
 )
}

export default Cabecalho;

Ou quem sabe executando diversos console.log() em funções que existem dentro dos componentes, de modo a identificar a quantidade de vezes em que ela está sendo chamada:

const rank = () => {
 console.log("Calculando Rank... 10");//Usando a estratégia do console.log!
 return 10;
}

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

Nesta lição você aprendeu a fazer o uso de novos hooks como:

  • useCallback
  • React.memo
  • useMemo
  • useRef

Onde cada um deles tem seu papel fundamental na otimização da performance das nossas aplicações 😉

Na próxima lição vamos aprender sobre o famoso hook chamado de useContext(), te aguardo lá!