useState e useEffect: uso correto

1. Visão geral

useState e useEffect são os dois hooks mais básicos do React, mas também são onde surgem mais bugs: component renderizando infinitamente, state não atualizando como esperado, effect rodando repetidamente sem necessidade. Este artigo percorre o mecanismo real de funcionamento dos dois hooks, patterns de uso correto e os erros comuns que você deve evitar. Depois de ler, você terá mais confiança para decidir “eu realmente preciso de useEffect aqui?” — pergunta que muitos devs React ainda respondem errado.

2. useState básico

useState retorna um par [value, setter]. Cada vez que você chama o setter, React agenda uma nova renderização do component com o novo valor.

import { useState } from "react";

function CustomerCounter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Número de clientes: {count}</p>
      <button onClick={() => setCount(count + 1)}>Adicionar cliente</button>
    </div>
  );
}
  • useState(0) inicializa count = 0 apenas na primeira renderização.
  • setCount(count + 1) cria uma nova renderização com count aumentando em 1.
  • count no closure da renderização atual é imutável — ele não muda sozinho entre duas renderizações.

Nota: Chamar setCount não altera o valor de count imediatamente na mesma renderização. React agrupa as atualizações e renderiza depois.

3. useState: atualizando baseado no state anterior

Se você chamar o setter múltiplas vezes na mesma função event handler baseado em count (variável já “congelada”), o resultado será incorreto.

function handleTripleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // Resultado: count aumenta apenas 1, não 3
}

Use atualização funcional para pegar o valor mais recente correto:

function handleTripleClick() {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  // Resultado: count aumenta corretamente 3
}
  • Quando você passa uma função para o setter, React garante que prev é sempre o valor state mais recente no momento do processamento, não o valor capturado no closure antigo.
  • Regra: se o novo state depende do state anterior, sempre use atualização funcional.

4. useState: lazy initial state

Se o valor inicial precisa ser calculado com custo alto, não chame a função diretamente — passe uma função para useState para que execute apenas uma vez.

function parseInitialOrders(raw) {
  console.log("Parseando dados de pedidos...");
  return JSON.parse(raw);
}

function OrderList({ rawOrders }) {
  // ERRADO: parseInitialOrders executa em toda renderização
  // const [orders, setOrders] = useState(parseInitialOrders(rawOrders));

  // CORRETO: executa apenas na primeira renderização
  const [orders, setOrders] = useState(() => parseInitialOrders(rawOrders));

  return <p>Número de pedidos: {orders.length}</p>;
}
  • A forma errada chama parseInitialOrders em toda renderização, mesmo que React só use o resultado da primeira vez.
  • A forma correta passa uma referência de função; React só a invoca quando monta pela primeira vez.

5. useEffect básico

useEffect executa um side effect depois que o component renderizar (depois que o DOM foi atualizado).

import { useEffect, useState } from "react";

function PageTitleUpdater({ orderCount }) {
  useEffect(() => {
    document.title = `Você tem ${orderCount} pedidos`;
  });

  return <p>Pedido atual: {orderCount}</p>;
}
  • Sem dependency array (segunda parte), esse effect executa após toda renderização — tanto mount quanto update.
  • document.title é um side effect típico: interage com o mundo fora do React (API DOM), não é apenas calcular um valor para renderizar.

6. useEffect: dependency array

O dependency array decide quando o effect executa. Existem 3 casos que você precisa memorizar bem.

6.1. Sem dependency array

useEffect(() => {
  console.log("Executa após TODA renderização");
});

6.2. Dependency array vazio []

useEffect(() => {
  console.log("Executa UMA VEZ depois de montar");
}, []);

6.3. Dependency array com valores

function CustomerProfile({ customerId }) {
  const [customer, setCustomer] = useState(null);

  useEffect(() => {
    fetchCustomer(customerId).then(setCustomer);
  }, [customerId]);

  return <p>{customer?.name ?? "Carregando..."}</p>;
}
  • O effect executa novamente apenas quando customerId muda entre duas renderizações (comparado com Object.is).
  • Se você esquecer customerId no array, o effect usará o valor antigo de customerId (stale closure) — buscará o cliente errado quando a prop mudar.

Nota: O plugin ESLint eslint-plugin-react-hooks com a regra exhaustive-deps o avisará quando você esquecer uma dependency. Não desative essa regra a menos que entenda bem as consequências.

7. useEffect: cleanup function

Quando um effect cria uma subscription, timer ou event listener, você deve retornar uma função cleanup para evitar vazamentos.

function OrderStatusListener({ orderId }) {
  const [status, setStatus] = useState("pending");

  useEffect(() => {
    const subscription = subscribeToOrderStatus(orderId, setStatus);

    return () => {
      subscription.unsubscribe();
    };
  }, [orderId]);

  return <p>Status: {status}</p>;
}
  • A função cleanup executa antes do effect executar novamente (quando orderId muda) e quando o component desmontar.
  • Se não limpar, cada vez que orderId muda criará uma nova subscription — eventualmente setStatus será chamado de muitas subscriptions antigas, causando atualização incorreta ou memory leak.
Ordem de execução quando orderId muda de 1 -> 2:
1. cleanup do effect com orderId = 1
2. novo effect executa com orderId = 2

8. Erros comuns

8.1. Infinite loop por falta de dependency correta

function BadExample() {
  const [data, setData] = useState({ count: 0 });

  useEffect(() => {
    // objeto novo a cada renderização -> effect sempre vê "mudança" se colocar data nas deps
    setData({ count: data.count + 1 }); // causa re-render -> effect executa novamente -> infinito
  }, [data]);

  return <p>{data.count}</p>;
}

Corrija usando atualização funcional e separando a condição de parada, ou remova completamente useEffect se essa lógica não precisa de um side effect real.

8.2. Stale closure em setTimeout/setInterval

function Countdown() {
  const [seconds, setSeconds] = useState(10);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds((prev) => prev - 1); // use atualização funcional, NÃO use seconds diretamente
    }, 1000);

    return () => clearInterval(id);
  }, []); // deps vazio porque não lê seconds diretamente no effect

  return <p>{seconds}</p>;
}
  • Se você escrever setSeconds(seconds - 1) em vez de atualização funcional, o closure sempre “lembrará” seconds = 10 da primeira vez que o effect rodou — a contagem regressiva fica presa no valor errado.

8.3. Usar useEffect para calcular um valor derivado

// ERRADO: redundante, causa renderização extra
function OrderSummaryBad({ items }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(items.reduce((sum, i) => sum + i.price, 0));
  }, [items]);

  return <p>Total: {total}</p>;
}

// CORRETO: calcula direto durante renderização
function OrderSummaryGood({ items }) {
  const total = items.reduce((sum, i) => sum + i.price, 0);
  return <p>Total: {total}</p>;
}

A versão “ERRADA” desperdiça uma renderização extra: renderiza com total antigo, o effect executa, setTotal dispara uma segunda renderização com o valor correto. A versão “CORRETA” calcula durante a renderização, sem effect, sem state extra.

9. Quando NÃO usar useEffect

Nota: useEffect é apenas para sincronizar com sistemas fora do React (DOM, API, subscription, timer). Se você puder calcular um valor diretamente a partir de props/state atuais, não use effect.

Três sinais de que você está usando errado:

  • Você usa effect para calcular um valor derivado de outro state (use uma variável comum ou useMemo).
  • Você usa effect para lidar com eventos do usuário (lide diretamente no handler onClick, onSubmit).
  • Você usa effect para resetar state quando a prop muda — geralmente pode usar a prop key para React remountar o component automaticamente em vez de usar effect.
// Em vez de usar effect para resetar o formulário quando customerId muda:
// useEffect(() => { setForm(initialForm); }, [customerId]);

// Use key para remountar o component, React reseta todo o state interno automaticamente:
<CustomerForm key={customerId} customerId={customerId} />

10. Testando o comportamento do hook

Use Jest junto com React Testing Library para verificar o comportamento real em vez de detalhes de implementação.

npm install --save-dev @testing-library/react @testing-library/user-event jest
// CustomerCounter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomerCounter } from "./CustomerCounter";

test("aumenta count ao clicar no botão Adicionar cliente", async () => {
  const user = userEvent.setup();
  render(<CustomerCounter />);

  expect(screen.getByText("Número de clientes: 0")).toBeInTheDocument();

  await user.click(screen.getByRole("button", { name: "Adicionar cliente" }));

  expect(screen.getByText("Número de clientes: 1")).toBeInTheDocument();
});
  • O teste chama render e depois simula um clique real com userEvent, assim como um usuário faria.
  • A assertion se baseia no conteúdo exibido (getByText), não no state interno — essa é a forma correta de testar React: teste comportamento, não implementação.
PASS  ./CustomerCounter.test.jsx
  ✓ aumenta count ao clicar no botão Adicionar cliente (45 ms)

11. Conclusão

useState é simples na superfície mas fácil de errar quando você esquece que o valor no closure é “congelado” a cada renderização — use atualização funcional quando o novo state depende do estado anterior. useEffect é poderoso mas deve ser usado apenas para sincronizar com sistemas fora do React; se um valor pode ser calculado diretamente durante a renderização, não o coloque dentro de um effect. Dominar o dependency array e a função cleanup evitará a maioria dos bugs comuns relacionados a esses dois hooks.

reacthooksjavascriptfrontendtypescript