Javascript

React Router: navegação em SPAs Já leu

14 min de leitura

React Router: navegação em SPAs
Até agora construímos componentes React que vivem em uma única página. Mas aplicações reais têm múltiplas rotas — /login, /dashboard, /produtos/42, /perfil. Sem recarregar o navegador. Isso é uma SPA — Single Page Applic

Até agora construímos componentes React que vivem em uma única página. Mas aplicações reais têm múltiplas rotas — /login, /dashboard, /produtos/42, /perfil. Sem recarregar o navegador.

Isso é uma SPA — Single Page Application. O HTML é carregado uma vez e o JavaScript controla qual conteúdo exibir conforme a URL muda. O React Router é a biblioteca padrão para isso no ecossistema React.


Como a navegação de SPA funciona

Aplicação tradicional (Multi-Page):
  URL muda → Servidor recebe requisição → Retorna HTML completo → Página recarrega

SPA com React Router:
  URL muda → JavaScript intercepta → Renderiza componente correto → Sem reload
  
  O servidor só é chamado para APIs — nunca para HTML das rotas

Instalando o React Router v6

npm install react-router-dom

Configuração básica

// src/main.jsx
import { BrowserRouter } from 'react-router-dom';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
// src/App.jsx — definindo as rotas
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Inicio from './pages/Inicio';
import Produtos from './pages/Produtos';
import DetalheProduto from './pages/DetalheProduto';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import NaoEncontrado from './pages/NaoEncontrado';

export default function App() {
  return (
    <Routes>
      {/* Rota com Layout compartilhado */}
      <Route path="/" element={<Layout />}>
        <Route index element={<Inicio />} />           {/* / */}
        <Route path="produtos" element={<Produtos />} /> {/* /produtos */}
        <Route path="produtos/:id" element={<DetalheProduto />} /> {/* /produtos/42 */}
        <Route path="dashboard" element={<Dashboard />} />
      </Route>

      {/* Rota sem Layout */}
      <Route path="/login" element={<Login />} />

      {/* Rota curinga — 404 */}
      <Route path="*" element={<NaoEncontrado />} />
    </Routes>
  );
}

Layout compartilhado com Outlet

// src/components/Layout.jsx
import { Outlet, NavLink } from 'react-router-dom';

export default function Layout() {
  return (
    <div className="layout">
      <header className="cabecalho">
        <nav className="navegacao">
          {/* NavLink adiciona classe "active" automaticamente na rota atual */}
          <NavLink
            to="/"
            className={({ isActive }) => isActive ? 'nav-link ativo' : 'nav-link'}
            end  // "end" garante que só ativa em "/" exato, não em "/produtos"
          >
            Início
          </NavLink>

          <NavLink
            to="/produtos"
            className={({ isActive }) => isActive ? 'nav-link ativo' : 'nav-link'}
          >
            Produtos
          </NavLink>

          <NavLink
            to="/dashboard"
            className={({ isActive }) => isActive ? 'nav-link ativo' : 'nav-link'}
          >
            Dashboard
          </NavLink>
        </nav>
      </header>

      <main className="conteudo">
        {/* Outlet renderiza o componente filho da rota atual */}
        <Outlet />
      </main>

      <footer className="rodape">
        <p>© 2025 Minha Empresa</p>
      </footer>
    </div>
  );
}

Hooks de navegação

import {
  useNavigate,
  useParams,
  useSearchParams,
  useLocation,
  Link,
} from 'react-router-dom';

// ── useNavigate — navegar programaticamente ────────
function FormularioLogin() {
  const navigate = useNavigate();

  async function handleLogin(dados) {
    await login(dados);
    navigate('/dashboard');             // navega para /dashboard
    navigate(-1);                       // volta uma página (como o botão voltar)
    navigate('/login', { replace: true }); // substitui no histórico (sem voltar)
    navigate('/produtos', {
      state: { mensagem: 'Login realizado!' } // passa estado pela navegação
    });
  }
}

// ── useParams — parâmetros da URL ──────────────────
function DetalheProduto() {
  const { id } = useParams();
  // URL: /produtos/42 → id = "42"
  // URL: /usuarios/ana/pedidos/99 → { usuarioId: "ana", pedidoId: "99" }

  const { data: produto } = useQuery({
    queryKey: ['produtos', id],
    queryFn: () => buscarProdutoPorId(id),
  });

  return <h1>{produto?.nome}</h1>;
}

// ── useSearchParams — query string ─────────────────
function ListaProdutos() {
  const [searchParams, setSearchParams] = useSearchParams();

  // URL: /produtos?categoria=eletronicos&pagina=2
  const categoria = searchParams.get('categoria') || '';
  const pagina = Number(searchParams.get('pagina')) || 1;

  function mudarCategoria(nova) {
    setSearchParams(prev => {
      prev.set('categoria', nova);
      prev.set('pagina', '1');  // volta para página 1 ao filtrar
      return prev;
    });
  }

  function mudarPagina(nova) {
    setSearchParams(prev => { prev.set('pagina', String(nova)); return prev; });
  }

  return (
    <div>
      <select value={categoria} onChange={e => mudarCategoria(e.target.value)}>
        <option value="">Todas</option>
        <option value="eletronicos">Eletrônicos</option>
        <option value="roupas">Roupas</option>
      </select>
      {/* lista de produtos */}
    </div>
  );
}

// ── useLocation — localização atual ────────────────
function PaginaQualquer() {
  const location = useLocation();

  console.log(location.pathname);  // "/produtos"
  console.log(location.search);    // "?categoria=eletronicos"
  console.log(location.state);     // estado passado via navigate()
  console.log(location.hash);      // "#secao"

  // Exemplo: redirecionar de volta para a página que o usuário tentou acessar
  const de = location.state?.de || '/';
  return <p>Você veio de: {de}</p>;
}

Rotas protegidas — autenticação

// src/components/RotaProtegida.jsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import useAuthStore from '../stores/authStore';

// Rota que exige login
export function RotaProtegida() {
  const estaLogado = useAuthStore((state) => !!state.token);
  const location = useLocation();

  if (!estaLogado) {
    // Salva a rota que o usuário tentou acessar para redirecionar depois do login
    return <Navigate to="/login" state={{ de: location.pathname }} replace />;
  }

  return <Outlet />;
}

// Rota que exige papel específico
export function RotaAdmin() {
  const usuario = useAuthStore((state) => state.usuario);
  const estaLogado = !!useAuthStore((state) => state.token);
  const location = useLocation();

  if (!estaLogado) {
    return <Navigate to="/login" state={{ de: location.pathname }} replace />;
  }

  if (usuario?.papel !== 'admin') {
    return <Navigate to="/acesso-negado" replace />;
  }

  return <Outlet />;
}

// Rota pública — redireciona se já estiver logado
export function RotaPublica() {
  const estaLogado = useAuthStore((state) => !!state.token);

  if (estaLogado) {
    return <Navigate to="/dashboard" replace />;
  }

  return <Outlet />;
}
// src/App.jsx — usando as rotas protegidas
import { Routes, Route } from 'react-router-dom';
import { RotaProtegida, RotaAdmin, RotaPublica } from './components/RotaProtegida';

export default function App() {
  return (
    <Routes>
      {/* Rotas públicas — redireciona se logado */}
      <Route element={<RotaPublica />}>
        <Route path="/login" element={<Login />} />
        <Route path="/cadastro" element={<Cadastro />} />
      </Route>

      {/* Rotas que exigem login */}
      <Route element={<RotaProtegida />}>
        <Route element={<Layout />}>
          <Route path="/" element={<Inicio />} />
          <Route path="/produtos" element={<Produtos />} />
          <Route path="/produtos/:id" element={<DetalheProduto />} />
          <Route path="/perfil" element={<Perfil />} />
        </Route>
      </Route>

      {/* Rotas que exigem papel admin */}
      <Route element={<RotaAdmin />}>
        <Route element={<LayoutAdmin />}>
          <Route path="/admin" element={<PainelAdmin />} />
          <Route path="/admin/usuarios" element={<GerenciarUsuarios />} />
          <Route path="/admin/produtos" element={<GerenciarProdutos />} />
        </Route>
      </Route>

      {/* Rotas de erro */}
      <Route path="/acesso-negado" element={<AcessoNegado />} />
      <Route path="*" element={<NaoEncontrado />} />
    </Routes>
  );
}

Login com redirecionamento inteligente

// src/pages/Login.jsx
import { useNavigate, useLocation } from 'react-router-dom';
import useAuthStore from '../stores/authStore';

export default function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login, carregando, erro } = useAuthStore();

  // Para onde redirecionar após o login
  const destino = location.state?.de || '/dashboard';

  const [form, setForm] = useState({ email: '', senha: '' });

  async function handleSubmit(e) {
    e.preventDefault();
    const resultado = await login(form.email, form.senha);
    if (resultado.sucesso) {
      navigate(destino, { replace: true }); // replace: não deixa voltar para /login
    }
  }

  return (
    <div className="pagina-login">
      <h1>Entrar</h1>
      {destino !== '/dashboard' && (
        <p className="aviso">Faça login para acessar {destino}</p>
      )}
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Email"
          value={form.email}
          onChange={e => setForm(p => ({ ...p, email: e.target.value }))}
        />
        <input
          type="password"
          placeholder="Senha"
          value={form.senha}
          onChange={e => setForm(p => ({ ...p, senha: e.target.value }))}
        />
        {erro && <p className="erro">{erro}</p>}
        <button type="submit" disabled={carregando}>
          {carregando ? 'Entrando...' : 'Entrar'}
        </button>
      </form>
      <p>Não tem conta? <Link to="/cadastro">Cadastre-se</Link></p>
    </div>
  );
}

Lazy loading — carregamento sob demanda

// src/App.jsx — carregando páginas apenas quando necessário
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// lazy() — só importa o componente quando a rota for acessada
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Produtos = lazy(() => import('./pages/Produtos'));
const DetalheProduto = lazy(() => import('./pages/DetalheProduto'));
const PainelAdmin = lazy(() => import('./pages/admin/PainelAdmin'));

// Componente de loading enquanto o chunk carrega
function CarregandoPagina() {
  return (
    <div className="pagina-loading">
      <div className="spinner" />
      <p>Carregando...</p>
    </div>
  );
}

export default function App() {
  return (
    // Suspense — exibe fallback enquanto o lazy component carrega
    <Suspense fallback={<CarregandoPagina />}>
      <Routes>
        <Route element={<RotaProtegida />}>
          <Route element={<Layout />}>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/produtos" element={<Produtos />} />
            <Route path="/produtos/:id" element={<DetalheProduto />} />
          </Route>
        </Route>
        <Route element={<RotaAdmin />}>
          <Route path="/admin" element={<PainelAdmin />} />
        </Route>
        <Route path="/login" element={<Login />} />
        <Route path="*" element={<NaoEncontrado />} />
      </Routes>
    </Suspense>
  );
}

// Benefício: o bundle inicial é menor
// /admin só é baixado se o usuário for admin e acessar a rota

Rotas aninhadas — breadcrumbs e sub-navegação

// src/pages/Produtos.jsx — com sub-rotas
import { Routes, Route, NavLink, Outlet, useParams } from 'react-router-dom';

// /produtos                → lista
// /produtos/novo           → formulário de criação
// /produtos/:id            → detalhe
// /produtos/:id/editar     → formulário de edição

export default function Produtos() {
  return (
    <div className="pagina-produtos">
      <div className="produtos-header">
        <h1>Produtos</h1>
        <Link to="novo" className="btn-primario">+ Novo Produto</Link>
      </div>
      <Outlet />  {/* renderiza ListaProdutos, DetalheProduto ou FormularioProduto */}
    </div>
  );
}

// No App.jsx
<Route path="produtos" element={<Produtos />}>
  <Route index element={<ListaProdutos />} />           {/* /produtos */}
  <Route path="novo" element={<FormularioProduto />} /> {/* /produtos/novo */}
  <Route path=":id" element={<DetalheProduto />} />     {/* /produtos/42 */}
  <Route path=":id/editar" element={<FormularioProduto />} /> {/* /produtos/42/editar */}
</Route>

Exemplo completo — SPA de e-commerce

// src/App.jsx — estrutura completa da aplicação
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RotaProtegida, RotaAdmin, RotaPublica } from './components/RotaProtegida';

const queryClient = new QueryClient();

// Páginas carregadas sob demanda
const Inicio       = lazy(() => import('./pages/Inicio'));
const Login        = lazy(() => import('./pages/Login'));
const Cadastro     = lazy(() => import('./pages/Cadastro'));
const Dashboard    = lazy(() => import('./pages/Dashboard'));
const Produtos     = lazy(() => import('./pages/Produtos'));
const DetalheProd  = lazy(() => import('./pages/DetalheProduto'));
const Perfil       = lazy(() => import('./pages/Perfil'));
const Admin        = lazy(() => import('./pages/admin/Painel'));
const NaoEncontrado = lazy(() => import('./pages/NaoEncontrado'));

// Layouts
const Layout       = lazy(() => import('./components/Layout'));
const LayoutAdmin  = lazy(() => import('./components/LayoutAdmin'));

function Spinner() {
  return <div className="pagina-loading"><div className="spinner" /></div>;
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <Suspense fallback={<Spinner />}>
          <Routes>

            {/* ── Públicas (redireciona se logado) ───── */}
            <Route element={<RotaPublica />}>
              <Route path="/login"    element={<Login />} />
              <Route path="/cadastro" element={<Cadastro />} />
            </Route>

            {/* ── Protegidas com Layout principal ─────── */}
            <Route element={<RotaProtegida />}>
              <Route element={<Layout />}>
                <Route index element={<Navigate to="/dashboard" replace />} />
                <Route path="/dashboard"         element={<Dashboard />} />
                <Route path="/produtos"           element={<Produtos />} />
                <Route path="/produtos/:id"       element={<DetalheProd />} />
                <Route path="/perfil"             element={<Perfil />} />
              </Route>
            </Route>

            {/* ── Admin com Layout próprio ─────────────── */}
            <Route element={<RotaAdmin />}>
              <Route element={<LayoutAdmin />}>
                <Route path="/admin"              element={<Admin />} />
                <Route path="/admin/:secao"       element={<Admin />} />
              </Route>
            </Route>

            {/* ── Erros ───────────────────────────────── */}
            <Route path="*" element={<NaoEncontrado />} />

          </Routes>
        </Suspense>
      </BrowserRouter>
    </QueryClientProvider>
  );
}

Scroll to top e transições de página

// src/components/ScrollToTop.jsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }, [pathname]);

  return null; // não renderiza nada
}

// No Layout
function Layout() {
  return (
    <>
      <ScrollToTop />
      <Cabecalho />
      <main><Outlet /></main>
      <Rodape />
    </>
  );
}
// Transição animada entre páginas com CSS
// src/components/PageTransition.jsx
import { useLocation } from 'react-router-dom';

export function PageTransition({ children }) {
  const { pathname } = useLocation();

  return (
    <div
      key={pathname}                    // muda a key → React remonta → CSS anima
      className="page-transition"
    >
      {children}
    </div>
  );
}

/* CSS */
/* .page-transition {
  animation: fadeIn 0.2s ease-in-out;
}
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
} */

Configurando o servidor para SPAs

Um ponto crucial que pega muitos iniciantes:

Problema:
  Usuário acessa diretamente /produtos/42 no navegador
  → Servidor procura o arquivo /produtos/42/index.html
  → Não existe → 404

Solução:
  Configurar o servidor para SEMPRE retornar index.html
  O React Router cuida do resto
# Nginx
location / {
  try_files $uri $uri/ /index.html;
}
// Express (para servir o build do React)
app.use(express.static('dist'));
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
# Vercel (vercel.json)
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
# Netlify (_redirects)
/*  /index.html  200

Tarefa para você

Construa a SPA completa do e-commerce integrando tudo dos módulos 6:

// Estrutura de rotas:
// /                    → redireciona para /dashboard (se logado) ou /login
// /login               → página de login
// /cadastro            → página de cadastro
// /dashboard           → visão geral (produtos, tarefas, stats)
// /produtos            → lista com filtros e paginação
// /produtos/:id        → detalhe do produto
// /produtos/novo       → criar produto (admin)
// /produtos/:id/editar → editar produto (admin)
// /tarefas             → lista de tarefas
// /perfil              → dados do usuário logado
// /admin               → painel administrativo
// /admin/usuarios      → gerenciar usuários

// Requisitos:
// 1. Lazy loading em todas as páginas
// 2. Rotas protegidas para usuário logado
// 3. Rotas de admin com verificação de papel
// 4. Redirecionar para a rota tentada após login
// 5. NavLink com active state no menu
// 6. Breadcrumbs que refletem a rota atual
// 7. ScrollToTop entre navegações
// 8. 404 personalizado com link para voltar
// 9. Zustand para auth, React Query para dados
// 10. Todos os filtros na URL (useSearchParams)

Conclusão

Neste artigo você aprendeu:

  • Como SPAs funcionam — JavaScript controla a navegação sem reload
  • Configuração básica do React Router v6 com BrowserRouter
  • Routes e Route — definindo o mapa de rotas da aplicação
  • Layout compartilhado com Outlet — evitando repetição de código
  • NavLink, Link e useNavigate — três formas de navegar
  • useParams, useSearchParams e useLocation — lendo dados da URL
  • Rotas protegidas — RotaProtegida, RotaAdmin e RotaPublica
  • Login com redirecionamento inteligente para a rota tentada
  • Lazy loading com React.lazy e Suspense — bundles menores
  • Rotas aninhadas — sub-navegação com Outlet aninhado
  • Scroll to top e transições animadas entre páginas
  • Configuração de servidor para SPAs — o erro mais comum em produção

No próximo artigo encerramos o Módulo 6 com a revisão completa e o projeto final — uma SPA completa com todas as tecnologias do módulo integradas.


📌 Próximo artigo: Aula 42 — Revisão do Módulo 6 + Projeto final: SPA completa


📚 Fontes e Referências

  • React Router v6 — Documentação oficial: https://reactrouter.com/en/main
  • React Router — Tutorial: https://reactrouter.com/en/main/start/tutorial
  • React Router — Rotas protegidas: https://reactrouter.com/en/main/start/faq
  • React — lazy e Suspense: https://react.dev/reference/react/lazy
  • Vite — Code Splitting: https://vitejs.dev/guide/build.html#chunking-strategy
  • The Road to React — Robin Wieruch: https://www.roadtoreact.com
  • Full Stack React — Anthony Accomazzo (Fullstack.io)
  • roadmap.sh — React: https://roadmap.sh/react
Comentários

Mais em Javascript

WebSockets e Comunicação em Tempo Real
WebSockets e Comunicação em Tempo Real

Artigo 50 — WebSockets e Comunicação em Tempo RealPor Prof. Ricardo Matos | M...

Objetos: estruturando dados do mundo real
Objetos: estruturando dados do mundo real

Arrays s&atilde;o &oacute;timos para listas. Mas como representar uma&nbsp;pe...

Introdução ao Node.js: JavaScript fora do navegador
Introdução ao Node.js: JavaScript fora do navegador

Até agora todo o JavaScript que escrevemos rodou no navegador. O navegador é...