Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State Já leu

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 ,

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.

Referências


Artigos relacionados