React in practice: hooks (useState, useEffect) and what they solve
Last lesson we saw that useState gives a component memory. Today we go deeper on that idea and get into the second most-used React hook, useEffect — and, more importantly, we understand the real problem each one solves, comparing them with the "by hand" way of solving the same problem in plain JavaScript.
01What a hook actually is
A hook is a special React function, always prefixed with use, that lets a function component "plug into" React's own capabilities — internal memory, lifecycle, shared context, among others. Before hooks (introduced in 2019), those capabilities only existed in class components, a more verbose syntax that was harder to reuse across different components.
Golden rule
Hooks can only be called at the top level of a component (never inside an if, loops, or nested functions), and always in the same order on every render. React uses that order to know which piece of memory belongs to which hook call — breaking this rule causes hard-to-trace bugs.
02useState revisited: the problem it solves
In plain JavaScript, if you want to store a value that changes and reflect that change on screen, you have to do it manually: store the value in a variable, write a function that updates the DOM whenever the value changes, and remember to call that function everywhere the value gets changed.
// Plain JS — "by hand"
let count = 0;
const span = document.querySelector('#count');
const button = document.querySelector('#increment');
function updateScreen() {
span.textContent = count;
}
button.addEventListener('click', () => {
count++;
updateScreen(); // easy to forget this in a bigger app
});
Works fine on a simple screen. But in an application with dozens of values that change, each affecting different pieces of the screen, manually syncing "who depends on who" becomes a constant source of bugs — screens that don't update, or that update the wrong thing.
// With useState — React handles the sync
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
By calling setCount, you never manipulate the DOM directly. You just update the data, and describe (in JSX) what the screen should look like for any value of that data. React handles comparing before and after and applying only the necessary change.
03useEffect: syncing with the world outside React
useEffect is the hook used for anything that isn't purely "calculate what to show on screen" — API calls, browser event subscriptions, timers, direct DOM manipulation outside React's control, logging, and so on. This kind of code is called a side effect: something that happens as a consequence of rendering, but isn't the rendering itself.
function UserProfile({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; }; // cleanup
}, [id]); // reruns only when "id" changes
if (!user) return <p>Loading...</p>;
return <h2>{user.name}</h2>;
}
The second argument — the [id] array, called the dependency array — tells React when to rerun the effect. Without that array, the effect runs after every render. With an empty array [], it runs only once, when the component first appears on screen.
04The problem useEffect solves
In plain JS, fetching data and keeping it in sync with the interface requires manually controlling the lifecycle: when the element enters the page, fetch the data; when the element leaves the page (or the parameters change), cancel the in-flight fetch to avoid updating a screen that no longer exists.
Without useEffect (by hand)
You need to know exactly when the element was inserted and removed from the DOM, usually using custom events, MutationObserver, or logic scattered across several places in the code.
With useEffect
React already knows when the component "is born," when it "dies," and when its dependencies change — and calls your effect function (and its cleanup function) at the right moments automatically.
05The most common mistake: incomplete dependency array
The classic mistake for people learning useEffect is using a variable inside the effect without declaring it in the dependency array. The effect keeps "seeing" the old value of that variable (a problem known as a stale closure), because React only reruns the function when something in the array changes.
// Wrong: "id" is used inside the effect, but isn't in the dependencies
useEffect(() => {
fetch(`/api/users/${id}`);
}, []); // will never refetch if "id" changes
// Right:
useEffect(() => {
fetch(`/api/users/${id}`);
}, [id]);
Tools like ESLint (with React's official hooks plugin) automatically warn when a dependency is missing — it's always worth keeping that warning enabled in the project.
06useState vs useEffect, side by side
Hook
Solves
When to use
useState
Storing and updating a value that affects what appears on screen
Counters, form fields, selected tabs, growing lists
useEffect
Syncing the component with something outside React
API calls, event subscriptions, timers, integrations with external libraries
07Pros and cons of hooks
Advantage
Why
Logic reuse
Custom hooks let you extract stateful logic (e.g. useClickCounter) and reuse it across components with no inheritance or wrapper components
Less code
Replace much of the verbosity of class components (constructor, this, separate lifecycle methods)
Function components only
You no longer need to choose between class or function — function always works, with hooks covering the same cases
Disadvantage
Why
Strict rules
Call order and the ban on conditional hooks confuse beginners
useEffect is easy to misuse
Incomplete dependencies, effects that run more often than they should, or logic that should live outside React
Less direct debugging
Multiple hooks interacting (state + effect + context) can make the data flow harder to trace than simple imperative code
08Comparison with alternatives
Not every framework handles "state + syncing with the outside world" the same way React does:
Approach
How it handles the state/effect equivalent
React (hooks)
Special functions (useState, useEffect) inside function components
Vue (Composition API)
A similar concept with ref/reactive for state and watchEffect for effects — very close in spirit to hooks
Svelte
Native language reactivity: a regular variable already triggers a screen update when it changes, with no explicit hook
Plain JS
Everything manual: variables, DOM update functions called explicitly, manual lifecycle control
To remember
useState solves the problem of keeping the screen in sync with a changing value, without manual DOM manipulation. useEffect solves the problem of syncing the component with anything outside React's control — APIs, timers, browser events — automatically handling when to run and when to clean up. Together, they replace much of the "plumbing" code you'd otherwise have to write by hand in plain JavaScript.
Share:
Newsletter
Learn what AI is creating for you. Don't get lost.
Get notified when a new deep dive or lesson goes up. No spam, just new posts.
Cooldecode · Lição 05 · Trilha principal
React na prática: hooks (useState, useEffect) e o que eles resolvem
Na lição passada vimos que useState dá memória a um componente. Hoje aprofundamos essa ideia e entramos no segundo hook mais usado do React, o useEffect — e, mais importante, entendemos o problema real que cada um resolve, comparando com o jeito "na mão" de resolver o mesmo problema em JavaScript puro.
01O que é um hook, de fato
Um hook é uma função especial do React, sempre com prefixo use, que permite a um componente de função "se conectar" a recursos do React — memória interna, ciclo de vida, contexto compartilhado, entre outros. Antes dos hooks (introduzidos em 2019), esses recursos só existiam em componentes de classe, uma sintaxe mais verbosa e mais difícil de reaproveitar entre componentes diferentes.
Regra de ouro
Hooks só podem ser chamados no nível mais alto de um componente (nunca dentro de if, laços ou funções aninhadas) e sempre na mesma ordem a cada renderização. O React usa essa ordem para saber qual pedaço de memória pertence a qual chamada de hook — quebrar essa regra gera bugs difíceis de rastrear.
02useState revisitado: o problema que ele resolve
Em JavaScript puro, se você quiser guardar um valor que muda e refletir essa mudança na tela, precisa fazer isso manualmente: guardar o valor numa variável, escrever uma função que atualiza o DOM sempre que o valor muda, e lembrar de chamar essa função em todo lugar onde o valor é alterado.
// JS puro — "na mão"
let contador = 0;
const span = document.querySelector('#contador');
const botao = document.querySelector('#incrementar');
function atualizarTela() {
span.textContent = contador;
}
botao.addEventListener('click', () => {
contador++;
atualizarTela(); // fácil de esquecer isso em um app maior
});
Funciona bem numa tela simples. Mas em uma aplicação com dezenas de valores que mudam, cada um afetando pedaços diferentes da tela, sincronizar manualmente "quem depende de quem" vira uma fonte constante de bugs — telas que não atualizam, ou que atualizam a coisa errada.
// Com useState — o React cuida da sincronização
function Contador() {
const [contador, setContador] = useState(0);
return (
<div>
<span>{contador}</span>
<button onClick={() => setContador(contador + 1)}>Incrementar</button>
</div>
);
}
Ao chamar setContador, você nunca manipula o DOM diretamente. Você só atualiza o dado, e descreve (no JSX) como a tela deveria se parecer para qualquer valor desse dado. O React cuida de comparar o antes e o depois e aplicar só a mudança necessária.
03useEffect: sincronizando com o mundo fora do React
useEffect é o hook usado para tudo que não é puramente "calcular o que mostrar na tela" — chamadas de API, assinaturas de eventos do navegador, timers, manipulação direta do DOM fora do controle do React, logging, e por aí vai. Esse tipo de código é chamado de efeito colateral (side effect): algo que acontece como consequência da renderização, mas que não é a renderização em si.
function PerfilUsuario({ id }) {
const [usuario, setUsuario] = useState(null);
useEffect(() => {
let cancelado = false;
fetch(`/api/usuarios/${id}`)
.then(res => res.json())
.then(dados => {
if (!cancelado) setUsuario(dados);
});
return () => { cancelado = true; }; // limpeza
}, [id]); // roda de novo só quando "id" muda
if (!usuario) return <p>Carregando...</p>;
return <h2>{usuario.nome}</h2>;
}
O segundo argumento — o array [id], chamado de array de dependências — diz ao React quando reexecutar o efeito. Sem esse array, o efeito roda depois de toda renderização. Com um array vazio [], roda só uma vez, quando o componente aparece na tela pela primeira vez.
04O problema que useEffect resolve
Em JS puro, buscar dados e sincronizá-los com a interface exige controlar manualmente o ciclo de vida: quando o elemento entrou na página, buscar os dados; quando o elemento sai da página (ou os parâmetros mudam), cancelar a busca em andamento para evitar atualizar uma tela que já não existe mais.
Sem useEffect (na mão)
Você precisa saber exatamente quando o elemento foi inserido e removido do DOM, geralmente usando eventos customizados, MutationObserver ou lógica espalhada em vários lugares do código.
Com useEffect
O React já sabe quando o componente "nasce", quando "morre" e quando suas dependências mudam — e chama sua função de efeito (e a função de limpeza) nos momentos certos automaticamente.
05O erro mais comum: array de dependências incompleto
O erro clássico de quem está aprendendo useEffect é usar uma variável dentro do efeito sem declará-la no array de dependências. O efeito continua "enxergando" o valor antigo dessa variável (um problema conhecido como stale closure), porque o React só reexecuta a função quando algo do array muda.
// Errado: "id" é usado dentro do efeito, mas não está nas dependências
useEffect(() => {
fetch(`/api/usuarios/${id}`);
}, []); // nunca vai buscar de novo se "id" mudar
// Certo:
useEffect(() => {
fetch(`/api/usuarios/${id}`);
}, [id]);
Ferramentas como o ESLint (com o plugin oficial de hooks do React) avisam automaticamente quando uma dependência está faltando — vale sempre manter esse aviso ativado no projeto.
06useState vs useEffect, lado a lado
Hook
Resolve
Quando usar
useState
Guardar e atualizar um valor que afeta o que aparece na tela
Contadores, campos de formulário, abas selecionadas, listas que crescem
useEffect
Sincronizar o componente com algo fora do React
Chamadas de API, assinaturas de eventos, timers, integrações com bibliotecas externas
07Prós e contras dos hooks
Vantagem
Por quê
Reaproveitamento de lógica
Hooks customizados permitem extrair lógica com estado (ex: useContadorDeCliques) e reusar entre componentes sem herança nem componentes wrapper
Menos código
Substituem boa parte da verbosidade dos componentes de classe (construtor, this, métodos de ciclo de vida separados)
Componentes de função apenas
Não é mais preciso decidir entre classe ou função — função sempre serve, com hooks cobrindo os mesmos casos
Desvantagem
Por quê
Regras rígidas
Ordem de chamada e proibição de hooks condicionais confundem iniciantes
useEffect é fácil de usar mal
Dependências incompletas, efeitos que rodam mais vezes do que deveriam, ou lógica que deveria estar fora do React
Debugging menos direto
Múltiplos hooks interagindo (state + effect + context) podem tornar o fluxo de dados mais difícil de rastrear que um código imperativo simples
08Comparação com alternativas
Nem todo framework resolve "estado + sincronização com o mundo externo" da mesma forma que o React:
Abordagem
Como lida com o equivalente a state/effect
React (hooks)
Funções especiais (useState, useEffect) dentro de componentes de função
Vue (Composition API)
Conceito parecido com ref/reactive para estado e watchEffect para efeitos — muito próximo em espírito aos hooks
Svelte
Reatividade nativa da linguagem: uma variável comum já dispara atualização de tela quando muda, sem hook explícito
JS puro
Tudo manual: variáveis, funções de atualização de DOM chamadas explicitamente, controle manual de ciclo de vida
Para fixar
useState resolve o problema de manter a tela sincronizada com um dado que muda, sem manipulação manual do DOM. useEffect resolve o problema de sincronizar o componente com qualquer coisa fora do controle do React — API, timers, eventos do navegador — cuidando automaticamente de quando rodar e quando limpar. Juntos, eles substituem boa parte do código de "encanamento" que seria necessário escrever à mão em JavaScript puro.
Compartilhar:
Newsletter
Saiba o que a IA está criando pra você. Não fique por fora.
Seja avisado quando eu postar uma imersão ou lição nova. Sem spam, só posts novos.