React Hooks: erros comuns e como evitar re-renderizações desnecessárias
1. O Ciclo de Vida das Re-renderizações no React Moderno
1.1. O que dispara uma re-renderização: estado, props e contexto
No React, uma re-renderização ocorre quando há mudanças no estado local de um componente, nas props recebidas de um componente pai, ou no valor de um Contexto do qual o componente é consumidor. Compreender esses gatilhos é o primeiro passo para evitar renderizações desnecessárias.
1.2. Diferença entre renderização "necessária" e "desnecessária"
Uma renderização necessária reflete uma mudança real na interface do usuário. Já uma renderização desnecessária ocorre quando o Virtual DOM é recalculado sem que haja alterações visíveis no DOM real. O custo disso é perceptível em componentes complexos ou listas grandes.
1.3. Ferramentas de debug: React DevTools Profiler e console.log estratégico
Use o React DevTools Profiler para gravar interações e identificar componentes que renderizam sem necessidade. Adicione console.log('Renderizou:', nomeDoComponente) no corpo do componente para rastrear visualmente o fluxo.
function MeuComponente() {
console.log('Renderizou: MeuComponente');
return <div>Olá</div>;
}
2. Erro Clássico: Dependências Incorretas no useEffect
2.1. Arrays de dependência vazios vs. ausentes: efeitos colaterais inesperados
Um array de dependências vazio ([]) executa o efeito apenas na montagem. Um array ausente executa o efeito em toda renderização, potencialmente criando loops infinitos se o efeito modificar o estado.
// Erro: sem array de dependências
useEffect(() => {
setCount(count + 1);
}); // Executa em toda renderização → loop infinito
// Correto: dependência explícita
useEffect(() => {
setCount(count + 1);
}, [count]);
2.2. Captura de valores obsoletos (stale closures) e como corrigir com useCallback/useRef
Closures obsoletas ocorrem quando um efeito captura o valor de uma variável no momento da criação, não no momento da execução.
function Contador() {
const [count, setCount] = useState(0);
// Erro: closure obsoleto
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count sempre será 0
}, 1000);
return () => clearInterval(timer);
}, []);
// Correto: usando função de atualização
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
}
2.3. Efeitos em cascata: como quebrar loops infinitos de re-renderização
Efeitos que alteram estado, que por sua vez disparam o mesmo efeito, criam loops. A solução é restringir as dependências ou usar useReducer para lógicas complexas.
3. O Abuso do useState e o Impacto no Desempenho
3.1. Estado derivado vs. estado armazenado: quando não usar useState
Se um valor pode ser calculado a partir de props ou de outro estado, não o armazene em useState. Calcule-o diretamente durante a renderização.
function Lista({ itens }) {
// Erro: estado desnecessário
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(itens.reduce((soma, item) => soma + item.valor, 0));
}, [itens]);
// Correto: valor derivado
const total = itens.reduce((soma, item) => soma + item.valor, 0);
return <div>Total: {total}</div>;
}
3.2. Atualizações em lote (batching) e o erro de múltiplos setState consecutivos
O React 18 agrupa atualizações de estado em eventos síncronos. Chamar setState múltiplas vezes seguidas resulta em uma única renderização com o último valor, a menos que use funções de atualização.
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Resultado: incrementa apenas 1 vez (count + 1)
// Correto:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Resultado: incrementa 3 vezes
}
3.3. Objetos e arrays mutáveis: como evitar referências quebradas com Immer
Sempre crie novos objetos/arrays ao atualizar estado. Use Immer para simplificar a imutabilidade.
import { produce } from 'immer';
// Erro: mutação direta
const [usuario, setUsuario] = useState({ nome: 'João', idade: 30 });
usuario.idade = 31; // Não dispara re-renderização
// Correto com Immer
setUsuario(produce(usuario, draft => {
draft.idade = 31;
}));
4. useCallback e useMemo: Quando Usar (e Quando Evitar)
4.1. O custo de memorizar: medição antes da otimização prematura
Memorizar tem custo de memória e comparação de dependências. Sempre meça o desempenho antes de aplicar useCallback ou useMemo.
4.2. Casos reais de benefício: props passadas para React.memo ou listas grandes
useCallback é útil quando uma função é passada como prop para um componente memoizado. useMemo é útil para cálculos pesados.
const ListaMemoizada = React.memo(({ itens, onDelete }) => {
return itens.map(item => (
<Item key={item.id} dados={item} onDelete={onDelete} />
));
});
function App() {
const [itens, setItens] = useState([]);
const handleDelete = useCallback((id) => {
setItens(prev => prev.filter(item => item.id !== id));
}, []); // Função estável, não recriada em cada renderização
return <ListaMemoizada itens={itens} onDelete={handleDelete} />;
}
4.3. Armadilha comum: dependências instáveis anulando o propósito do useCallback
Se as dependências de useCallback mudam a cada renderização, a função memorizada perde o propósito.
// Erro: dependência instável anula memorização
const [dados, setDados] = useState({});
const funcao = useCallback(() => {
processar(dados);
}, [dados]); // dados é um novo objeto a cada renderização
5. Context API e Re-renderizações em Massa
5.1. Toda atualização de contexto re-renderiza todos os consumidores
Qualquer mudança no valor do contexto força todos os componentes consumidores a renderizarem, independentemente de usarem ou não a parte alterada.
5.2. Estratégias de mitigação: separação de contextos por domínio
Divida contextos grandes em contextos menores e especializados.
const TemaContext = createContext('claro');
const UsuarioContext = createContext(null);
function App() {
const [tema, setTema] = useState('claro');
const [usuario, setUsuario] = useState(null);
return (
<TemaContext.Provider value={tema}>
<UsuarioContext.Provider value={usuario}>
<Main />
</UsuarioContext.Provider>
</TemaContext.Provider>
);
}
5.3. Alternativas modernas: useSyncExternalStore e bibliotecas como Zustand
Para estado global complexo, considere useSyncExternalStore (React 18) ou Zustand, que oferecem assinaturas seletivas e evitam re-renderizações em massa.
6. Re-renderizações em Listas e Componentes Filhos
6.1. React.memo na prática: verificando props com função de comparação customizada
React.memo realiza comparação superficial de props. Para comparações profundas, forneça uma função customizada.
const Item = React.memo(({ produto }) => {
return <li>{produto.nome} - R${produto.preco}</li>;
}, (propsAnteriores, propsNovas) => {
return propsAnteriores.produto.id === propsNovas.produto.id;
});
6.2. O problema das funções inline em props de eventos e render props
Funções inline criam novas referências a cada renderização, quebrando a memoização.
// Erro: função inline
<Item onClick={() => handleClick(id)} />
// Correto: função memorizada
const handleClickItem = useCallback(() => handleClick(id), [id, handleClick]);
<Item onClick={handleClickItem} />
6.3. Chaves (keys) instáveis: como índices de array causam re-renderizações em cadeia
Usar índices como key causa problemas quando a ordem dos itens muda. Use IDs únicos e estáveis.
// Erro: key baseada em índice
{itens.map((item, index) => <Item key={index} dados={item} />)}
// Correto: key baseada em ID único
{itens.map(item => <Item key={item.id} dados={item} />)}
7. Padrões Avançados para Controle Fino de Renderização
7.1. useRef para valores mutáveis que não disparam re-renderização
Use useRef para armazenar valores que precisam persistir entre renderizações mas não afetam a UI.
function Timer() {
const intervalRef = useRef(null);
const [segundos, setSegundos] = useState(0);
const iniciar = () => {
intervalRef.current = setInterval(() => {
setSegundos(prev => prev + 1);
}, 1000);
};
const parar = () => {
clearInterval(intervalRef.current);
};
}
7.2. Separação de estado em componentes: o princípio da "single responsibility"
Cada componente deve gerenciar apenas o estado que lhe pertence. Extraia estado para componentes menores quando possível.
7.3. Virtualização de listas com react-window e lazy loading de componentes
Para listas extensas, use react-window para renderizar apenas os itens visíveis. Combine com React.lazy para carregamento sob demanda.
import { FixedSizeList as List } from 'react-window';
function ListaVirtualizada({ itens }) {
const Row = ({ index, style }) => (
<div style={style}>{itens[index].nome}</div>
);
return (
<List
height={400}
itemCount={itens.length}
itemSize={35}
width={300}
>
{Row}
</List>
);
}
Referências
- Documentação oficial do React: useCallback — Guia completo sobre o hook useCallback, com exemplos de quando memorizar funções e armadilhas comuns.
- React DevTools Profiler — Tutorial oficial sobre como usar o Profiler para identificar gargalos de desempenho e re-renderizações desnecessárias.
- A Complete Guide to useEffect — Artigo clássico de Dan Abramov explicando o ciclo de vida dos efeitos, closures e dependências.
- React.memo e otimização de desempenho — Tutorial prático sobre React.memo, comparação de props e quando evitar a memorização.
- Zustand: estado global sem re-renderizações desnecessárias — Documentação da biblioteca Zustand, alternativa à Context API para estado global com assinaturas seletivas.
- react-window: virtualização de listas — Documentação oficial da biblioteca react-window para renderizar listas grandes de forma eficiente.
- Immer: imutabilidade simplificada no React — Guia completo sobre como usar Immer para gerenciar estado imutável sem boilerplate.