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)inicializacount = 0apenas na primeira renderização.setCount(count + 1)cria uma nova renderização comcountaumentando em 1.countno closure da renderização atual é imutável — ele não muda sozinho entre duas renderizações.
Nota: Chamar
setCountnão altera o valor decountimediatamente 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
parseInitialOrdersem 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
customerIdmuda entre duas renderizações (comparado comObject.is). - Se você esquecer
customerIdno array, o effect usará o valor antigo decustomerId(stale closure) — buscará o cliente errado quando a prop mudar.
Nota: O plugin ESLint
eslint-plugin-react-hookscom a regraexhaustive-depso 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
orderIdmuda) e quando o component desmontar. - Se não limpar, cada vez que
orderIdmuda criará uma nova subscription — eventualmentesetStatusserá 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 = 10da 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
keypara 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
rendere depois simula um clique real comuserEvent, 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.