Aprenda / Frontend — Tailwind & Next.js

IntermediárioAula 12 de 2015 min0% da trilha

useMemo, useCallback e useRef

"Usa sempre que puder" é o conselho errado — cada um resolve um problema bem específico, e usar sem necessidade também tem custo.

É comum ouvir "usa useMemo e useCallback em tudo, por garantia". Isso é conselho ruim: os dois têm um custo real (guardar valor em memória, comparar o array de dependências a cada render), e cada um existe pra resolver um problema específico — não pra "otimizar" de forma genérica.

useMemo — cachear um valor calculado caro

Sem useMemo, uma computação cara dentro do componente roda de novo a cada render — inclusive quando o render foi causado por algo que não tem nada a ver com aquele cálculo.

Sem useMemo — recalcula sempre
function ListaDeProdutos({ produtos, termoBusca }: Props) {
  const [tema, setTema] = useState("light"); // mudar isso recalcula a lista inteira também

  const filtrados = produtos
    .filter((p) => p.nome.includes(termoBusca))
    .sort((a, b) => a.preco - b.preco); // caro se a lista for grande

  return <ul>{filtrados.map((p) => <li key={p.id}>{p.nome}</li>)}</ul>;
}
Com useMemo — só recalcula quando produtos ou termoBusca mudam
function ListaDeProdutos({ produtos, termoBusca }: Props) {
  const [tema, setTema] = useState("light");

  const filtrados = useMemo(
    () => produtos.filter((p) => p.nome.includes(termoBusca)).sort((a, b) => a.preco - b.preco),
    [produtos, termoBusca]
  );

  return <ul>{filtrados.map((p) => <li key={p.id}>{p.nome}</li>)}</ul>;
}

useCallback — cachear a identidade de uma função

useCallback não cacheia um valor calculado — ele cacheia a referência de uma função, pra ela não virar uma função "nova" a cada render. Isso importa quando essa função é prop de um componente memoizado com React.memo, ou dependência de outro hook.

Sem useCallback — memo do filho não adianta
const BotaoCurtir = memo(function BotaoCurtir({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>❤ Curtir</button>;
});

function Post({ id }: { id: string }) {
  const [tema, setTema] = useState("light");
  const curtir = () => enviarLike(id); // nova função a cada render de Post

  return <BotaoCurtir onClick={curtir} />; // memo não evita re-render: a prop "onClick" sempre muda
}
Com useCallback — a referência se mantém entre renders
function Post({ id }: { id: string }) {
  const [tema, setTema] = useState("light");
  const curtir = useCallback(() => enviarLike(id), [id]);

  return <BotaoCurtir onClick={curtir} />; // agora o memo funciona de verdade
}

useRef — valor mutável que não dispara render

useState guarda valor e causa re-render quando muda. useRef guarda valor e não causa re-render — útil pra coisas que o componente precisa lembrar entre renders, mas que não fazem parte do que é exibido na tela: o ID de um setTimeout, o valor anterior de uma prop, uma referência a um elemento do DOM.

useRef — guardando um id de timeout sem re-renderizar
function CampoComAutoSalvar({ valor, onSalvar }: Props) {
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  function handleChange(novoValor: string) {
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => onSalvar(novoValor), 800);
  }

  return <input defaultValue={valor} onChange={(e) => handleChange(e.target.value)} />;
}

O custo de otimizar sem necessidade

useMemo e useCallback não são "grátis": React precisa guardar o valor anterior e comparar o array de dependências a cada render. Pra um cálculo barato (somar dois números, formatar uma string curta), esse custo de comparação pode ser maior que o custo de simplesmente recalcular. A regra prática: meça antes de otimizar (React DevTools Profiler mostra onde o tempo realmente vai) — não adicione os dois hooks por reflexo em todo componente que você escrever.

Exercício

No componente abaixo, ordenarPedidos é uma função cara e ListaPedidos é memoizado com React.memo. Toda vez que o componente pai re-renderiza (por qualquer motivo), a lista inteira é reordenada e o memo do filho não evita re-render nenhum. Reescreva usando useMemo e useCallback onde fizer sentido.

Quiz rápido

O que useCallback memoriza, diferente do useMemo?
Por que usar useMemo e useCallback em todo lugar 'por garantia' pode ser pior do que não usar?