<h2>Introdução: Os Quatro Pilares do Gerenciamento de Estado</h2>
<p>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.</p>
<p>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.</p>
<h2>Estado Local: O Escopo do Componente</h2>
<h3>Conceito Fundamentais</h3>
<p>Estado local é informação que pertence exclusivamente a um componente e seus filhos imediatos. Quando você usa <code>useState</code>, 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.</p>
<p>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.</p>
<h3>Exemplo Prático: Componente de Abas</h3>
<pre><code class="language-javascript">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;</code></pre>
<p>Neste exemplo, o <code>activeTab</code> é 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.</p>
<h3>Quando Elevar o Estado</h3>
<p>À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.</p>
<pre><code class="language-javascript">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;</code></pre>
<h2>Estado Global: Compartilhamento Entre Componentes Distantes</h2>
<h3>Quando e Por Que Usar</h3>
<p>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.</p>
<p>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.</p>
<h3>Context API com Padrão Profissional</h3>
<pre><code class="language-javascript">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;
}</code></pre>
<p>Na sua aplicação:</p>
<pre><code class="language-javascript">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>
);
}</code></pre>
<h3>Evitando Renders Desnecessários</h3>
<p>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 <code>useMemo</code> para otimizar:</p>
<pre><code class="language-javascript">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>
);
}</code></pre>
<h2>Estado do Servidor: Sincronizando com APIs</h2>
<h3>O Paradigma Moderno</h3>
<p>Estado do servidor é qualquer dado que vem de um backend ou API externa. A chave está em entender que <strong>dados no servidor e dados no cliente podem divergir</strong>. 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.</p>
<p>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.</p>
<h3>Implementação Manual com useState e useEffect</h3>
<pre><code class="language-javascript">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;</code></pre>
<h3>Implementação Profissional com React Query</h3>
<pre><code class="language-javascript">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;</code></pre>
<h3>Padrão de Cache e Revalidação</h3>
<p>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:</p>
<pre><code class="language-javascript">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();</code></pre>
<h2>Estado da URL: Sincronizando com a Barra de Endereço</h2>
<h3>Por Que a URL Importa</h3>
<p>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.</p>
<h3>Implementação com React Router v6</h3>
<pre><code class="language-javascript">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;</code></pre>
<p>Quando o usuário clica em "Ação", a URL muda para <code>?genero=acao&pagina=1</code>. Se compartilhar esse link, qualquer pessoa verá os mesmos resultados.</p>
<h3>Padrão Avançado: Sincronizando Múltiplas Fontes</h3>
<p>Em aplicações reais, você frequentemente precisa sincronizar estado local, URL e servidor. Um padrão robusto:</p>
<pre><code class="language-javascript">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;</code></pre>
<p>Neste padrão, o que é digitado no input atualiza <code>termoBuscado</code> (estado local). Após 500ms, isso dispara uma busca no servidor. A resposta atualiza tanto <code>resultados</code> quanto a URL. Se o usuário sair e voltar, a URL preserva a busca.</p>
<h2>Conclusão</h2>
<p>Você aprendeu os quatro pilares do gerenciamento de estado em React. <strong>Primeiro</strong>, estado local com <code>useState</code> é sua ferramenta padrão para informações exclusivas de um componente. <strong>Segundo</strong>, estado global via Context API ou bibliotecas como Zustand resolve compartilhamento eficiente entre componentes distantes, mas deve ser usado com parcimônia. <strong>Terceiro</strong>, 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. <strong>Quarto</strong>, 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/learn/scaling-up-with-reducer-and-context" target="_blank" rel="noopener noreferrer">React Documentation - State Management</a></li>
<li><a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">TanStack Query (React Query) Documentation</a></li>
<li><a href="https://reactrouter.com/en/main/hooks/use-search-params" target="_blank" rel="noopener noreferrer">React Router useSearchParams</a></li>
<li><a href="https://github.com/pmndrs/zustand" target="_blank" rel="noopener noreferrer">Zustand - State Management</a></li>
<li><a href="https://kentcdodds.com/blog/how-to-use-react-context-effectively" target="_blank" rel="noopener noreferrer">Context API Deep Dive - Kent C. Dodds</a></li>
</ul>
<p><!-- FIM --></p>