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 RouteseRoute— definindo o mapa de rotas da aplicaçãoLayoutcompartilhado comOutlet— evitando repetição de códigoNavLink,LinkeuseNavigate— três formas de navegaruseParams,useSearchParamseuseLocation— lendo dados da URL- Rotas protegidas —
RotaProtegida,RotaAdmineRotaPublica - Login com redirecionamento inteligente para a rota tentada
- Lazy loading com
React.lazyeSuspense— bundles menores - Rotas aninhadas — sub-navegação com
Outletaninhado - 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