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