Introdução: Os Quatro Pilares do Gerenciamento de Estado
O gerenciamento de estado é um dos desafios fundamentais no desenvolvimento de aplicações React modernas. Quando você constrói uma interface que responde a interações do usuário, a informação sobre o que está acontecendo naquele momento precisa estar em algum lugar — esse "lugar" é chamado de estado. O problema começa quando você tem muitas informações em diferentes partes da aplicação e precisa decidir onde armazená-las.
Existem quatro categorias principais de estado em React, cada uma com seu propósito específico: estado local (component state), estado global (compartilhado entre múltiplos componentes), estado do servidor (dados vindos de APIs) e estado da URL (informações codificadas na barra de endereço). Entender quando usar cada um é o que separa desenvolvedores iniciantes de profissionais produtivos. Vamos explorar cada camada em detalhe.
Estado Local: O Escopo do Componente
Conceito Fundamentais
Estado local é informação que pertence exclusivamente a um componente e seus filhos imediatos. Quando você usa useState, você está criando estado local. Essa abordagem é simples, rápida e não requer dependências externas. A regra de ouro é: se apenas um componente precisa dessa informação, ou apenas pai-filho-neto precisam, use estado local.
Um erro comum é tentar centralizar tudo em estado global desde o início. Na prática, a maioria dos estados das suas aplicações deve ser local. Isso torna componentes reutilizáveis, testáveis e independentes.
Exemplo Prático: Componente de Abas
import { useState } from 'react';
function TabsComponent() {
const [activeTab, setActiveTab] = useState('home');
const tabs = ['home', 'perfil', 'configuracoes'];
return (
<div className="tabs">
<div className="tab-buttons">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={activeTab === tab ? 'active' : ''}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
<div className="tab-content">
{activeTab === 'home' && <div>Conteúdo da Home</div>}
{activeTab === 'perfil' && <div>Conteúdo do Perfil</div>}
{activeTab === 'configuracoes' && <div>Conteúdo de Configurações</div>}
</div>
</div>
);
}
export default TabsComponent;
Neste exemplo, o activeTab é puramente local. Nenhum outro componente fora deste precisa saber qual aba está ativa. Se você precisar passar essa informação para um componente filho, use props. Se um primo ou tio precisar, aí sim você move para estado global.
Quando Elevar o Estado
Às vezes você cria o estado em um componente e depois descobre que um irmão também precisa daquele valor. A solução é "elevar" o estado para o pai comum mais próximo. Isso é explicitamente documentado na documentação oficial do React e é uma habilidade essencial.
import { useState } from 'react';
// Componente pai
function FormularioLogin() {
const [email, setEmail] = useState('');
const [senha, setSenha] = useState('');
return (
<div>
<CampoEmail valor={email} onChange={setEmail} />
<CampoSenha valor={senha} onChange={setSenha} />
<BotaoEnvio email={email} senha={senha} />
</div>
);
}
// Componentes filhos recebem valores via props
function CampoEmail({ valor, onChange }) {
return (
<input
type="email"
value={valor}
onChange={(e) => onChange(e.target.value)}
placeholder="seu@email.com"
/>
);
}
function CampoSenha({ valor, onChange }) {
return (
<input
type="password"
value={valor}
onChange={(e) => onChange(e.target.value)}
placeholder="Sua senha"
/>
);
}
function BotaoEnvio({ email, senha }) {
return (
<button onClick={() => console.log(`Login: ${email}`)}>
Enviar
</button>
);
}
export default FormularioLogin;
Estado Global: Compartilhamento Entre Componentes Distantes
Quando e Por Que Usar
Estado global é necessário quando múltiplos componentes em diferentes partes da árvore precisam acessar a mesma informação. Exemplos clássicos: dados do usuário logado, tema da aplicação, configurações gerais, idioma, carrinhos de compra. A tentação de centralizar tudo em estado global é real, mas resistir a ela mantém seu código limpo.
Hoje você tem várias opções: Context API (nativa do React), Redux, Zustand, Recoil, Jotai. Vou focar em Context API com um padrão robusto, pois é nativa e suficiente para 80% dos casos.
Context API com Padrão Profissional
import { createContext, useContext, useState } from 'react';
// Criar o contexto
const UsuarioContext = createContext();
// Provider personalizado (melhor prática)
export function UsuarioProvider({ children }) {
const [usuario, setUsuario] = useState(null);
const [carregando, setCarregando] = useState(false);
const login = async (email, senha) => {
setCarregando(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, senha })
});
const dados = await response.json();
setUsuario(dados.usuario);
} catch (erro) {
console.error('Erro no login:', erro);
} finally {
setCarregando(false);
}
};
const logout = () => {
setUsuario(null);
};
const value = { usuario, carregando, login, logout };
return (
<UsuarioContext.Provider value={value}>
{children}
</UsuarioContext.Provider>
);
}
// Hook customizado para consumir (melhor prática)
export function useUsuario() {
const context = useContext(UsuarioContext);
if (!context) {
throw new Error('useUsuario deve ser usado dentro de UsuarioProvider');
}
return context;
}
Na sua aplicação:
function App() {
return (
<UsuarioProvider>
<Navbar />
<Main />
</UsuarioProvider>
);
}
// Em qualquer componente filho, acesse assim:
function PerfilUsuario() {
const { usuario, logout } = useUsuario();
if (!usuario) return <p>Não autenticado</p>;
return (
<div>
<h1>Bem-vindo, {usuario.nome}</h1>
<button onClick={logout}>Sair</button>
</div>
);
}
Evitando Renders Desnecessários
Uma armadilha comum é que qualquer mudança no contexto causa re-render de todos os componentes que o consomem, mesmo que aquele componente não dependa do valor específico que mudou. A solução é dividir contextos por domínio e usar useMemo para otimizar:
export function UsuarioProvider({ children }) {
const [usuario, setUsuario] = useState(null);
const [carregando, setCarregando] = useState(false);
// Memoizar o valor para evitar re-renders desnecessários
const value = useMemo(
() => ({ usuario, carregando, login, logout }),
[usuario, carregando]
);
return (
<UsuarioContext.Provider value={value}>
{children}
</UsuarioContext.Provider>
);
}
Estado do Servidor: Sincronizando com APIs
O Paradigma Moderno
Estado do servidor é qualquer dado que vem de um backend ou API externa. A chave está em entender que dados no servidor e dados no cliente podem divergir. Você baixa um valor, o usuário muda algo no servidor (via outro cliente, outro navegador), e seu cliente está desatualizado. Lidar com isso é crucial.
Bibliotecas modernas como React Query (TanStack Query), SWR e Apollo gerenciam cache, revalidação, sincronização e erro de forma elegante. Vou mostrar tanto uma implementação manual quanto com React Query.
Implementação Manual com useState e useEffect
import { useState, useEffect } from 'react';
function ListaProdutos() {
const [produtos, setProdutos] = useState([]);
const [carregando, setCarregando] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
const buscarProdutos = async () => {
try {
setCarregando(true);
const response = await fetch('/api/produtos');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const dados = await response.json();
setProdutos(dados);
setErro(null);
} catch (err) {
setErro(err.message);
setProdutos([]);
} finally {
setCarregando(false);
}
};
buscarProdutos();
}, []);
if (carregando) return <p>Carregando...</p>;
if (erro) return <p>Erro: {erro}</p>;
return (
<ul>
{produtos.map(produto => (
<li key={produto.id}>{produto.nome} - R$ {produto.preco}</li>
))}
</ul>
);
}
export default ListaProdutos;
Implementação Profissional com React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Funções de API isoladas
const produtosAPI = {
listar: async () => {
const res = await fetch('/api/produtos');
if (!res.ok) throw new Error('Erro ao buscar produtos');
return res.json();
},
atualizar: async (id, dados) => {
const res = await fetch(`/api/produtos/${id}`, {
method: 'PUT',
body: JSON.stringify(dados)
});
if (!res.ok) throw new Error('Erro ao atualizar');
return res.json();
}
};
function ListaProdutos() {
const queryClient = useQueryClient();
const { data: produtos, isLoading, error } = useQuery({
queryKey: ['produtos'],
queryFn: produtosAPI.listar,
staleTime: 1000 * 60 * 5, // 5 minutos
});
const atualizarMutation = useMutation({
mutationFn: ({ id, dados }) => produtosAPI.atualizar(id, dados),
onSuccess: () => {
// Revalidar a lista após sucesso
queryClient.invalidateQueries({ queryKey: ['produtos'] });
}
});
if (isLoading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error.message}</p>;
return (
<ul>
{produtos.map(produto => (
<li key={produto.id}>
{produto.nome}
<button
onClick={() => atualizarMutation.mutate({
id: produto.id,
dados: { nome: produto.nome + ' (atualizado)' }
})}
>
Atualizar
</button>
</li>
))}
</ul>
);
}
export default ListaProdutos;
Padrão de Cache e Revalidação
React Query brilha ao entender que você não quer sempre fazer requisições. Dados "stale" (desatualizados) podem ser servidos instantaneamente enquanto uma nova requisição acontece em background. Isso melhora a percepção de performance dramaticamente:
const { data, isStale, refetch } = useQuery({
queryKey: ['usuario', userId],
queryFn: () => fetch(`/api/usuarios/${userId}`).then(r => r.json()),
staleTime: 1000 * 60 * 10, // 10 minutos
gcTime: 1000 * 60 * 60, // Cache por 1 hora (antes era cacheTime)
});
// Forçar revalidação quando necessário
const handleRefresh = () => refetch();
Estado da URL: Sincronizando com a Barra de Endereço
Por Que a URL Importa
A URL é um estado compartilhado entre guias, histórico do navegador e compartilhamento. Se um usuário está filtrando uma lista por categoria e quer mandar o link para um colega, aquela filtragem precisa estar na URL. A URL é o estado mais "persistente" e "compartilhável" que você tem.
Implementação com React Router v6
import { useSearchParams } from 'react-router-dom';
function ListaFilmes() {
const [searchParams, setSearchParams] = useSearchParams();
const genero = searchParams.get('genero') || 'todos';
const pagina = parseInt(searchParams.get('pagina')) || 1;
const filtrar = (novoGenero) => {
setSearchParams({ genero: novoGenero, pagina: '1' });
};
const proximaPagina = () => {
setSearchParams({ genero, pagina: pagina + 1 });
};
return (
<div>
<div className="filtros">
<button onClick={() => filtrar('todos')}>Todos</button>
<button onClick={() => filtrar('acao')}>Ação</button>
<button onClick={() => filtrar('comedia')}>Comédia</button>
</div>
<p>Mostrando: {genero} - Página {pagina}</p>
<p>URL atual: {window.location.search}</p>
<button onClick={proximaPagina}>Próxima Página</button>
</div>
);
}
export default ListaFilmes;
Quando o usuário clica em "Ação", a URL muda para ?genero=acao&pagina=1. Se compartilhar esse link, qualquer pessoa verá os mesmos resultados.
Padrão Avançado: Sincronizando Múltiplas Fontes
Em aplicações reais, você frequentemente precisa sincronizar estado local, URL e servidor. Um padrão robusto:
import { useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
function PesquisaProdutos() {
const [searchParams, setSearchParams] = useSearchParams();
const [resultados, setResultados] = useState([]);
const [carregando, setCarregando] = useState(false);
const termo = searchParams.get('q') || '';
const categoriaUrl = searchParams.get('categoria') || 'todos';
// Estado local para debouncing
const [termoBuscado, setTermoBuscado] = useState(termo);
// Sincronizar URL com servidor quando termo ou categoria mudam
useEffect(() => {
const timer = setTimeout(() => {
if (termoBuscado || categoriaUrl !== 'todos') {
buscarProdutos();
}
}, 500); // Debounce de 500ms
return () => clearTimeout(timer);
}, [termoBuscado, categoriaUrl]);
const buscarProdutos = async () => {
setCarregando(true);
try {
const query = new URLSearchParams({
q: termoBuscado,
categoria: categoriaUrl
});
const response = await fetch(`/api/produtos?${query}`);
const dados = await response.json();
setResultados(dados);
// Atualizar URL após busca bem-sucedida
setSearchParams({ q: termoBuscado, categoria: categoriaUrl });
} finally {
setCarregando(false);
}
};
return (
<div>
<input
type="text"
value={termoBuscado}
onChange={(e) => setTermoBuscado(e.target.value)}
placeholder="Buscar..."
/>
<select
value={categoriaUrl}
onChange={(e) => setSearchParams({
q: termoBuscado,
categoria: e.target.value
})}
>
<option value="todos">Todas as categorias</option>
<option value="eletronicos">Eletrônicos</option>
<option value="livros">Livros</option>
</select>
{carregando && <p>Buscando...</p>}
<ul>
{resultados.map(produto => (
<li key={produto.id}>{produto.nome}</li>
))}
</ul>
</div>
);
}
export default PesquisaProdutos;
Neste padrão, o que é digitado no input atualiza termoBuscado (estado local). Após 500ms, isso dispara uma busca no servidor. A resposta atualiza tanto resultados quanto a URL. Se o usuário sair e voltar, a URL preserva a busca.
Conclusão
Você aprendeu os quatro pilares do gerenciamento de estado em React. Primeiro, estado local com useState é sua ferramenta padrão para informações exclusivas de um componente. Segundo, estado global via Context API ou bibliotecas como Zustand resolve compartilhamento eficiente entre componentes distantes, mas deve ser usado com parcimônia. Terceiro, estado do servidor com React Query ou SWR abstrai a complexidade de cache, sincronização e revalidação, tornando sua aplicação mais robusta e rápida. Quarto, URL state com React Router sincroniza sua aplicação com o navegador, permitindo compartilhamento e navegação por histórico. O segredo profissional é escolher a camada certa para cada dado — local para ui, global para contexto amplo, servidor para dados remotos, e URL para tudo que o usuário pode compartilhar.