Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

React Router: rotas, links e parâmetros na URL

Como configurar rotas no React Router 7, navegar sem recarregar a página, ler parâmetro da URL e proteger uma rota que exige login.

Rodolfo Mori19 min de leitura

Sem rotas, uma aplicação React pode até trocar de tela, mas a pessoa não consegue copiar o endereço daquela tela, atualizar a página no mesmo ponto nem usar o botão voltar como espera. O React Router resolve justamente isso: ele relaciona cada URL a um componente e mantém endereço, conteúdo e histórico caminhando juntos.

Neste artigo você vai montar as rotas de uma livraria online, a Página Sete. No fim, /livros/12 abrirá um livro específico, o carrinho terá endereço próprio, uma área de pedidos exigirá login e uma URL desconhecida mostrará o 404. Os exemplos foram executados com React 19.2.8, React Router 7.18.2, Vite 8.2.2 e Caddy 2.10.2 servindo o build.

Por que uma aplicação React precisa de rotas?

Ela precisa de rotas quando mais de uma tela deve ter endereço próprio. Uma rota é a regra que relaciona um caminho, como /livros, ao componente que será mostrado; o trecho /livros é o pathname, ou caminho da URL.

Pense na aplicação como uma livraria dentro de um único prédio. As rotas são as placas que indicam catálogo, detalhe do livro, carrinho e pedidos. Trocar de corredor não exige reconstruir o prédio: da mesma forma, uma navegação interna pode trocar o conteúdo sem baixar outro documento HTML. O histórico guarda o caminho percorrido, então voltar funciona, e a placa pode ser fotografada — ou a URL copiada — para outra pessoa chegar ao mesmo lugar.

Essa comparação tem um limite importante: a placa só aponta o destino. O React Router escolhe qual componente combina com a URL, mas não busca o livro no banco, não autentica o cliente e não configura o servidor de produção por você.

Uma SPA (Single Page Application, ou aplicação de página única) normalmente recebe um index.html e troca partes da interface com JavaScript. O React Router observa a URL, escolhe a rota correspondente e usa a History API do navegador para registrar a navegação sem pedir outro documento. Você também pode ler o caminho diretamente com window.location, mas teria de reconstruir por conta própria o casamento de rotas, os links e o histórico que a biblioteca já entrega.

bash
npm create vite@latest livraria-pagina-sete -- --template react
cd livraria-pagina-sete
npm install
npm install react-router@7.18.2
node -p "['react','react-dom','react-router','vite'].map(p => p + ' ' + require(p + '/package.json').version).join('\n')"
react 19.2.8 react-dom 19.2.8 react-router 7.18.2 vite 8.2.2

As quatro linhas confirmam exatamente o ambiente do teste: React e React DOM na 19.2.8, React Router na 7.18.2 e Vite na 8.2.2. O pacote instalado é react-router, sem -dom no fim. Se você seguiu a lição de criar projeto React com Vite, a novidade é somente a última instalação.

A documentação atual oferece três maneiras de organizar o mesmo roteamento:

  • no modo Declarative, <BrowserRouter> envolve a aplicação e <Routes> reúne componentes <Route>; é a entrada mais enxuta para rotas e links;
  • no modo Data, createBrowserRouter cria a configuração fora da árvore e <RouterProvider> a fornece à aplicação; ele também permite loader, action e estados de navegação;
  • no modo Framework, módulos de rota e o plugin do React Router acrescentam recursos como divisão de código, renderização no servidor e geração estática.

Esta lição usa o modo Data, mas começa apenas com caminhos e componentes. O bloco congelado importa RouterProvider de react-router, que é válido; em uma aplicação no navegador, a documentação atual recomenda normalmente importá-lo de react-router/dom por causa da integração com o React DOM. Se encontrar um tutorial com <BrowserRouter>, <Routes> e <Route>, não é outra biblioteca: é o modo Declarative. Escolha um roteador de topo para o projeto e siga o padrão dele.

Como o createBrowserRouter transforma URLs em telas?

Ele recebe uma árvore de objetos de rota e escolhe o element cujo path combina com a URL atual. O roteador criado por createBrowserRouter entra na aplicação por um RouterProvider.

No exemplo mínimo abaixo, cada objeto é como uma linha do mapa da livraria: path informa o endereço e element informa qual componente ocupa a tela.

jsx
// src/main.jsx
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import Home from './paginas/Home';
import Livros from './paginas/Livros';
import LivroDetalhe from './paginas/LivroDetalhe';
import Carrinho from './paginas/Carrinho';
import NaoEncontrada from './paginas/NaoEncontrada';

const rotas = [
  { path: '/', element: <Home /> },
  { path: '/livros', element: <Livros /> },
  { path: '/livros/:id', element: <LivroDetalhe /> },
  { path: '/carrinho', element: <Carrinho /> },
  { path: '*', element: <NaoEncontrada /> },
];

const router = createBrowserRouter(rotas);

createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);

Dois caminhos não são literais. :id é um segmento dinâmico: ele aceita um valor naquela posição e o guarda com o nome id. O * é um curinga: ele combina com o que não foi atendido pelas rotas mais específicas e mostra a tela de 404.

Para ver o roteador escolhendo, dá para rodar a mesma árvore fora do navegador. O createMemoryRouter guarda a URL na memória em vez de na barra de endereços, e aí renderToStaticMarkup mostra o HTML que cada caminho produz:

jsx
import { renderToStaticMarkup } from 'react-dom/server';
import { createMemoryRouter, RouterProvider } from 'react-router';

for (const url of ['/', '/livros', '/livros/12', '/livros/999', '/promocoes']) {
  const router = createMemoryRouter(rotas, { initialEntries: [url] });
  console.log(url.padEnd(13), '->', renderToStaticMarkup(<RouterProvider router={router} />));
}
/ -> <h1>Livraria Página Sete</h1> /livros -> <ul><li><a href="/livros/12" data-discover="true">Torto Arado</a></li><li><a href="/livros/27" data-discover="true">O Cortiço</a></li><li><a href="/livros/41" data-discover="true">Vidas Secas</a></li></ul> /livros/12 -> <article><h2>Torto Arado</h2><p>Itamar Vieira Junior</p><p>R$ 54.90</p></article> /livros/999 -> <p>Livro não encontrado no catálogo.</p> /promocoes -> <h2>Não existe nada em /promocoes</h2>

Leia a saída como um mapa sendo percorrido. / mostra o título da livraria; /livros mostra os três links; /livros/12 mostra Torto Arado. Em /livros/999, a rota dinâmica combinou e o componente rodou, mas ele não encontrou o dado. Já /promocoes não combinou com nenhuma rota específica e caiu no curinga. Casar uma rota e encontrar um registro são problemas diferentes.

A ordem das rotas muda qual tela aparece?

Neste exemplo, não: como as rotas têm especificidades diferentes, o React Router classifica cada uma antes de escolher. Por isso uma rota literal vence um segmento dinâmico, e o curinga perde para as duas. O exemplo põe * em primeiro lugar de propósito:

jsx
const rotas = [
  { path: '*', element: <p>404</p> },
  { path: '/livros/:id', element: <p>detalhe do livro</p> },
  { path: '/livros/novidades', element: <p>lançamentos do mês</p> },
];

// e o mesmo laço de createMemoryRouter + renderToStaticMarkup do bloco anterior
/livros/12 -> <p>detalhe do livro</p> /livros/novidades -> <p>lançamentos do mês</p> /promocoes -> <p>404</p>

As três linhas comprovam a regra: /livros/12 usa o segmento dinâmico, /livros/novidades prefere a rota literal e /promocoes sobra para o curinga. Você pode organizar o array para facilitar a leitura sem depender da posição para obter esse resultado. A posição ainda pode desempatar rotas irmãs com a mesma pontuação; evite caminhos equivalentes que disputem a mesma URL.

Experimente você mesmo

Antes de executar, faça a previsão: se a rota * continuar no início e você trocar /livros/novidades por /livros/promocoes, qual componente vencerá? No projeto Vite criado no início, substitua apenas o array rotas de src/main.jsx pelo segundo array, mantendo createBrowserRouter e RouterProvider. Rode npm run dev e digite /livros/novidades na barra do navegador: a rota literal deve mostrar “lançamentos do mês”. Teste depois /livros/qualquer-coisa, que deve mostrar o detalhe, e /promocoes, que deve mostrar o 404. Assim você executa JSX pelo Vite, sem depender de uma ferramenta extra para o Node entender JSX.

Até aqui, o mapa já tem endereços estáticos, um parâmetro dinâmico e uma saída para URLs desconhecidas. Agora falta transformar o menu em navegação interna.

Porque eles fazem a navegação pelo roteador e preservam a aplicação já carregada. Num clique comum em <a href="/carrinho">, o navegador pede outro documento, descarta o estado que só existia na memória, baixa os arquivos novamente e monta o React do zero.

O Link continua produzindo um link acessível no HTML, mas assume os cliques que podem ser tratados no cliente: atualiza o histórico e avisa o roteador sem pedir outro documento. Uma âncora <a> continua sendo a escolha certa para um site externo, um arquivo ou uma navegação que deve recarregar de propósito.

jsx
// src/paginas/Livros.jsx
import { Link } from 'react-router';
import { catalogo } from '../dados';

export default function Livros() {
  return (
    <ul>
      {catalogo.map((livro) => (
        <li key={livro.id}>
          <Link to={`/livros/${livro.id}`}>{livro.titulo}</Link>
        </li>
      ))}
    </ul>
  );
}

A key aí não tem nada a ver com rota — é a regra de renderizar listas no React, que continua valendo dentro do menu.

Para provar a diferença, montei a aplicação no jsdom (um navegador de mentira que roda no Node), contei quantas vezes o componente de casca montou e cliquei nos dois tipos de link:

jsx
let montagens = 0;

function Casca() {
  useEffect(() => {
    montagens += 1;
    console.log('Casca montou — total de montagens:', montagens);
  }, []);
  return (
    <div>
      <Link id="spa" to="/livros/12">Torto Arado (Link)</Link>
      <a id="cru" href="/carrinho">Carrinho (a)</a>
      <main><Outlet /></main>
    </div>
  );
}

Nas linhas abaixo, “na tela” é o textContent do <main>: somente o conteúdo da rota filha, sem os dois links da casca.

Casca montou — total de montagens: 1 inicio | URL: /livros | na tela: Torto AradoO CortiçoVidas Secas depois do Link | URL: /livros/12 | na tela: Torto AradoItamar Vieira JuniorR$ 54.90 montagens ate aqui: 1 depois do <a> | URL: /livros/12 | na tela: Torto AradoItamar Vieira JuniorR$ 54.90 [jsdom] Not implemented: navigation to another Document

Leia os quatro fatos da saída. A casca montou uma vez; o Link mudou a URL para /livros/12; o conteúdo virou o detalhe de Torto Arado; e a contagem continuou em 1. No clique da âncora, o jsdom avisou navigation to another Document porque não implementa a carga de outro documento. Num navegador real, um clique interno normal nessa âncora inicia justamente essa nova carga.

Use end quando o item deve ficar ativo somente na URL exata. O NavLink é um Link que também informa se o destino está ativo. Ele aplica a classe active e aria-current="page", informação importante para tecnologias assistivas. Sem end, uma rota descendente também mantém o item da seção ativo:

jsx
<NavLink to="/">Início</NavLink>
<NavLink to="/livros">Catálogo</NavLink>
<NavLink to="/livros" end>Catálogo (end)</NavLink>
/ -> ativo: Início /livros -> ativo: Catálogo + Catálogo (end) /livros/12 -> ativo: Catálogo

A saída mostra três regras. Em /, apenas “Início” fica ativo. Em /livros, os dois links de catálogo ficam ativos. Em /livros/12, o catálogo sem end continua aceso por representar a seção, enquanto o que tem end apaga. O próprio React Router trata to="/" como exceção: ele só fica ativo na raiz.

Experimente você mesmo

Antes de abrir o navegador, preveja em qual endereço “Catálogo” ficará ativo. Ainda em src/main.jsx, adicione NavLink ao import do React Router, envolva as três linhas do exemplo num componente Menu. Para isolar o teste, substitua temporariamente todo o array de rotas por uma única rota: [{ path: '*', element: <Menu /> }]. Rode npm run dev e visite primeiro /livros e depois /livros/12. O link com end deve ter active e aria-current="page" apenas na primeira URL. Remova end e repita: o menu de seção deve ficar ativo nas duas telas. Na próxima seção você restaurará as rotas e transformará esse menu temporário no Layout definitivo.

O que significa o erro que cita NavigationContext?

Ele significa que um componente de roteamento foi renderizado fora do roteador. Um Link sem o contexto fornecido pelo roteador quebra assim:

jsx
function Rodape() {
  return <Link to="/livros">Ver o catálogo</Link>;
}

console.log(renderToStaticMarkup(<Rodape />));
file:///private/tmp/livraria-pagina-sete/node_modules/react-router/dist/development/chunk-62JRHF6Z.mjs:10518 let { basename, navigator, useTransitions } = React10.useContext(NavigationContext); ^

TypeError: Cannot destructure property ‘basename’ of ‘React10.useContext(…)’ as it is null. at LinkWithRef (file:///private/tmp/livraria-pagina-sete/node_modules/react-router/dist/development/chunk-62JRHF6Z.mjs:10518:11)

A mensagem cita basename e NavigationContext, palavras que você não escreveu, porque o erro acontece dentro da biblioteca. O diagnóstico é: Link, NavLink, useNavigate ou useLocation tentaram ler o contexto do roteador e receberam null. useParams é diferente: fora de uma rota correspondente, ele pode devolver um objeto vazio em vez deste erro. Na aplicação, corrija mantendo o componente abaixo do RouterProvider; em teste, monte-o com um createMemoryRouter e seu provider. Componentes isolados no Storybook e uma segunda raiz criada fora do provider são outras causas comuns.

Resumo desta etapa: Link navega sem remontar a aplicação, NavLink acrescenta o estado ativo e todos eles precisam viver dentro de um roteador.

Como ler um parâmetro da URL com useParams?

Chame useParams dentro do componente da rota. Ele devolve um objeto em que a chave é o nome escrito depois de : no path e o valor é o texto recebido na URL.

jsx
// src/paginas/LivroDetalhe.jsx
import { useParams } from 'react-router';
import { buscarLivro } from '../dados';

export default function LivroDetalhe() {
  const { id } = useParams();
  const livro = buscarLivro(id);

  if (!livro) return <p>Livro não encontrado no catálogo.</p>;

  return (
    <article>
      <h2>{livro.titulo}</h2>
      <p>{livro.autor}</p>
      <p>R$ {livro.preco.toFixed(2)}</p>
    </article>
  );
}

O detalhe que costuma causar o defeito está no tipo. Um parâmetro presente chega como string, porque nasceu de um trecho da URL; se um segmento opcional não aparecer, seu valor pode ser undefined. O roteador não tem como adivinhar que 12 representa um número. O componente espião abaixo mostra o valor em duas rotas:

jsx
function Espiao() {
  const params = useParams();
  console.log('params:', JSON.stringify(params), '| typeof id:', typeof params.id);
  console.log('  buscando com id      :', catalogo.find((l) => l.id === params.id)?.titulo);
  console.log('  buscando com Number(id):', catalogo.find((l) => l.id === Number(params.id))?.titulo);
  return null;
}
URL /livros/12 params: {"id":"12"} | typeof id: string buscando com id : undefined buscando com Number(id): Torto Arado URL /autores/graciliano-ramos/livros/41 params: {"autor":"graciliano-ramos","id":"41"} | typeof id: string buscando com id : undefined buscando com Number(id): Vidas Secas

As duas primeiras linhas de cada URL confirmam que o objeto contém texto: "12" e "41", ambos do tipo string. Por isso l.id === params.id compara número com string e não encontra nada. Depois de Number(id), a mesma busca encontra Torto Arado e Vidas Secas. O diagnóstico é incompatibilidade de tipos; a correção é converter e validar o parâmetro na entrada, antes de usá-lo no restante da aplicação.

Como reutilizar o layout com rotas aninhadas e Outlet?

Coloque as rotas filhas em children e marque com <Outlet /> o ponto do layout onde a filha selecionada deve aparecer. Assim, cabeçalho, menu e rodapé ficam no elemento pai e não precisam ser repetidos em cada tela.

Voltando ao mapa da livraria, o layout é o salão fixo e o Outlet é o espaço de exposição que recebe o conteúdo do corredor atual. A ideia se aproxima da composição com children, mas quem escolhe o conteúdo aqui é a rota que combinou com a URL, não uma prop enviada manualmente.

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

export default function Layout() {
  return (
    <div>
      <header>
        <NavLink to="/">Início</NavLink>
        <NavLink to="/livros">Catálogo</NavLink>
        <NavLink to="/carrinho">Carrinho</NavLink>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>Livraria Página Sete — desde 2019</footer>
    </div>
  );
}
jsx
// src/rotas.jsx
export const rotas = [
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'livros', element: <Livros /> },
      { path: 'livros/:id', element: <LivroDetalhe /> },
      { path: 'carrinho', element: <Carrinho /> },
      { path: '*', element: <NaoEncontrada /> },
    ],
  },
];

Três detalhes trabalham juntos. Os caminhos dos filhos perderam a barra inicial porque são relativos ao pai: livros dentro de path: '/' forma /livros. index: true identifica a filha padrão, mostrada quando a URL é exatamente a do pai. E o * está entre os filhos, por isso até a tela de 404 aparece dentro do mesmo cabeçalho e rodapé.

Você até pode colocar dois <Outlet /> no mesmo layout, mas ambos renderizam a mesma filha que combinou com a URL. Eles não funcionam como espaços nomeados e independentes. Para controlar duas áreas diferentes, aninhe outro nível de rota ou componha uma das áreas com props.

Renderizando /livros/12 com essa árvore:

<div> <header> <a class="" href="/" data-discover="true">Início</a> <a aria-current="page" class="active" href="/livros" data-discover="true">Catálogo</a> <a class="" href="/carrinho" data-discover="true">Carrinho</a> </header> <main> <article> <h2>Torto Arado</h2> <p>Itamar Vieira Junior</p> <p>R$ 54.90</p> </article> </main> <footer>Livraria Página Sete — desde 2019</footer> </div>

Na saída, o <Outlet /> não aparece como tag: em seu lugar está o <article> de Torto Arado. O cabeçalho e o rodapé continuam ao redor, enquanto aria-current="page" marca “Catálogo” como ativo. data-discover é um atributo interno do React Router e não exige nenhuma ação sua.

Resumo do encaixe: o pai preserva a estrutura, a rota filha escolhe o conteúdo e o Outlet indica exatamente onde os dois se encontram.

Como navegar por código depois de enviar um formulário?

Obtenha a função de navegação com useNavigate e chame-a no evento que conclui a ação. É o caminho certo quando a mudança de tela depende de algo que seu código acabou de fazer, e não de um link escolhido antecipadamente.

jsx
function LivroDetalhe() {
  const { id } = useParams();
  const navegar = useNavigate();

  function aoEnviar(evento) {
    evento.preventDefault();
    const quantidade = Number(new FormData(evento.target).get('quantidade'));
    navegar('/carrinho', { state: { id: Number(id), quantidade } });
  }

  return (
    <form onSubmit={aoEnviar}>
      <input name="quantidade" type="number" defaultValue={2} />
      <button type="submit">Adicionar ao carrinho</button>
    </form>
  );
}

O segundo argumento aceita state, um dado salvo no histórico da navegação sem aparecer na URL. A tela de destino o lê com useLocation. No teste, o formulário foi enviado no jsdom:

antes | URL: /livros/12 | tela: Adicionar ao carrinho depois | URL: /carrinho | tela: Recebi no state: {"id":12,"quantidade":2}

A primeira linha mostra a rota do livro; a segunda confirma três mudanças: a URL virou /carrinho, a tela de destino foi renderizada e o objeto {"id":12,"quantidade":2} chegou inteiro pelo state. O preventDefault é necessário nesse exemplo: sem ele, o navegador envia o formulário como uma navegação de documento antes que o fluxo do cliente termine. Se houver campos controlados, revise formulário controlado no React.

Como esta lição usa o modo Data, prefira o componente <Form> do React Router, uma função action e redirect quando o próprio roteador gerenciar o envio. Nesse fluxo, o Form captura o envio, a action processa os dados e o redirect escolhe a próxima URL; o roteador também organiza estados de navegação e erros da rota. useNavigate continua apropriado para um evento imperativo da interface, como fechar um modal e seguir para outra tela; o formulário local acima o mantém visível para você entender a função isoladamente.

O aviso pode ser reproduzido chamando navegar('/carrinho') durante a renderização:

You should call navigate() in a React.useEffect(), not when your component is first rendered. URL: /livros/12

A saída informa a correção literalmente: chame em um evento ou em um efeito. Neste teste, a URL permaneceu em /livros/12 durante a primeira renderização. Isso não transforma o corpo do componente em um lugar válido para tentar de novo: navegar é um efeito colateral; renderizar deve apenas descrever a interface.

Como exigir login e voltar à rota que a pessoa tentou abrir?

Guarde a localização desejada no state, envie quem não tem sessão para o login e, depois da entrada, navegue de volta usando replace. O componente abaixo faz a primeira decisão: mostra o conteúdo para quem está autenticado ou renderiza um Navigate para quem precisa entrar.

jsx
function RotaPrivada({ children }) {
  const local = useLocation();
  if (!clienteLogado) {
    return <Navigate to="/entrar" state={{ de: local.pathname }} replace />;
  }
  return children;
}
jsx
const rotas = [
  { path: '/livros/:id', element: <Livro /> },
  { path: '/entrar', element: <Entrar /> },
  {
    path: '/conta/pedidos',
    element: (
      <RotaPrivada>
        <Pedidos />
      </RotaPrivada>
    ),
  },
];

<Navigate> é a alternativa declarativa à função de useNavigate: ao ser renderizado, ele muda a localização. O state leva o pathname que a pessoa tentou abrir para a tela de login saber qual é o destino de volta:

jsx
function Entrar() {
  const { state } = useLocation();
  const navegar = useNavigate();
  const destino = state?.de ?? '/';

  return (
    <button
      onClick={() => {
        clienteLogado = 'ana@paginasete.com.br';
        navegar(destino, { replace: true });
      }}
    >
      Entrar
    </button>
  );
}

O teste percorre quatro passos: a pessoa está num livro, tenta abrir “Meus pedidos”, entra e depois usa o botão voltar do navegador.

--- replace: true --- 1 inicio | URL: /livros/12 | tela: Meus pedidos Entrar recebeu state.de = "/conta/pedidos" 2 sem login | URL: /entrar | tela: Entrar 3 depois de entrar | URL: /conta/pedidos | tela: Pedidos de ana@paginasete.com.br 4 botao voltar | URL: /livros/12 | tela: Meus pedidos

Com replace: true, a quarta linha volta diretamente para /livros/12. A rota de login não ficou presa no histórico. Compare com o mesmo percurso usando uma navegação normal:

--- replace: false --- 1 inicio | URL: /livros/12 | tela: Meus pedidos Entrar recebeu state.de = "/conta/pedidos" 2 sem login | URL: /entrar | tela: Entrar 3 depois de entrar | URL: /conta/pedidos | tela: Pedidos de ana@paginasete.com.br Entrar recebeu state.de = "/conta/pedidos" 4 botao voltar | URL: /entrar | tela: Entrar

Sem replace, a linha 4 volta para /entrar, e o log mostra que essa tela recebeu de novo o destino /conta/pedidos. O histórico ficou com login e pedidos empilhados, criando o conhecido vai e volta entre as duas telas. O diagnóstico está na sequência das URLs; a correção é substituir a entrada de login em vez de adicionar outra.

Por que uma rota funciona no Vite e dá 404 depois do deploy?

Porque o servidor de produção pode procurar um arquivo chamado /livros/12 em vez de entregar o index.html para o React Router. No npm run dev, o Vite já faz esse fallback; uma hospedagem precisa ser configurada para fazer o mesmo.

Ao clicar num Link, a biblioteca trata a navegação no cliente. Ao recarregar /livros/12 ou abrir essa URL diretamente, porém, o primeiro pedido vai ao servidor. Como não existe dist/livros/12, ele devolve 404 antes que o JavaScript e o roteador tenham a chance de executar.

GET /livros/12 procura o arquivo dist/livros/12 não existe 404 GET /livros/12 try_files devolve dist/index.html o JS carrega o router mostra o livro sem try_files com try_files

O diagrama separa os dois fluxos. Em cima, o servidor procura um arquivo que não existe e encerra com 404. Embaixo, try_files devolve o index.html, o JavaScript carrega e só então o React Router escolhe o livro. O teste começa com o build e um Caddy configurado apenas para servir arquivos:

bash
npm run build
vite v8.2.2 building client environment for production... transforming... ✓ 29 modules transformed. rendering chunks... computing gzip size... dist/index.html 0.47 kB │ gzip: 0.30 kB dist/assets/index-nqMpL4T3.css 1.78 kB │ gzip: 0.81 kB dist/assets/index-DHNCI_kA.js 282.92 kB │ gzip: 90.11 kB

✓ built in 190ms

A saída confirma que o build terminou e criou dist/index.html, além dos arquivos reais de CSS e JavaScript. O primeiro teste serve exatamente essa pasta, mas ainda sem a regra de fallback. Salve o próximo bloco num arquivo chamado Caddyfile.errado, na raiz do projeto:

text
:4173 {
	root * dist
	file_server
}
bash
caddy run --config Caddyfile.errado --adapter caddyfile
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4173/
curl -si http://localhost:4173/livros/12 | head -12

Não cole as três linhas de uma vez. O caddy run fica executando para manter o servidor no ar: rode somente a primeira linha em um terminal, deixe-o aberto e execute os dois curl em um segundo terminal, dentro da mesma pasta.

200 HTTP/1.1 404 Not Found Server: Caddy Date: Sat, 22 Aug 2026 21:45:47 GMT Content-Length: 0

Leia os dois resultados: / responde 200, mas /livros/12 responde 404 Not Found com corpo vazio. Não é um erro do componente nem do array de rotas; o React Router não executou porque nenhum HTML chegou ao navegador. A correção é orientar o servidor a entregar index.html quando o caminho não corresponder a um arquivo real.

text
:4173 {
	root * dist
	try_files {path} /index.html
	file_server
}

Salve essa versão como Caddyfile. No primeiro terminal, pressione Ctrl+C para parar a configuração errada e inicie a nova com caddy run --config Caddyfile --adapter caddyfile. Só então execute, no segundo terminal, os dois comandos abaixo:

bash
curl -si http://localhost:4173/livros/12 | head -9
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://localhost:4173/assets/index-DHNCI_kA.js
HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 470 Content-Type: text/html; charset=utf-8 Etag: "dkvspqdwm97fd2" Last-Modified: Sat, 22 Aug 2026 21:38:05 GMT Server: Caddy Vary: Accept-Encoding Date: Sat, 22 Aug 2026 21:45:50 GMT 200 text/javascript; charset=utf-8

Agora /livros/12 responde 200 com os 470 bytes do index.html, enquanto o bundle continua respondendo 200 com text/javascript. Isso confirma que try_files preserva os arquivos reais e usa o HTML apenas como fallback. Depois de receber esse HTML, o navegador carrega o bundle e o React Router finalmente mostra o livro.

E o servidor de desenvolvimento, que nunca reclamou? Ele já fazia o mesmo fallback desde sempre:

bash
npm run dev -- --port 5173
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://localhost:5173/livros/12

Aqui também são dois terminais: rode npm run dev -- --port 5173 no primeiro e deixe o processo aberto; rode apenas o curl no segundo. Quando terminar, pressione Ctrl+C no primeiro terminal.

200 text/html

O 200 text/html comprova que o servidor de desenvolvimento já faz o fallback. Por isso o defeito só aparece no deploy quando a hospedagem não repete essa configuração.

O nome da diretiva muda de servidor para servidor, mas a ideia é sempre a mesma. Só a linha do Caddy foi medida aqui; as outras são a forma equivalente na documentação de cada um:

onde arquivo a linha
Caddy Caddyfile try_files {path} /index.html
Nginx bloco location / try_files $uri $uri/ /index.html;
Apache .htaccess FallbackResource /index.html
Netlify public/_redirects /* /index.html 200

O que você já consegue construir e qual é o próximo passo?

Você já consegue transformar URLs em telas, navegar com Link e NavLink, ler parâmetros, encaixar rotas filhas em um Outlet, redirecionar após ações, preservar o destino de login e corrigir o fallback de produção.

O mapa da livraria agora funciona do navegador ao servidor: cada corredor tem endereço, o histórico registra o caminho e uma URL desconhecida recebe uma tela própria. O limite continua valendo — o roteador organiza a navegação, mas estilo, dados e segurança pertencem a outras partes da aplicação.

A próxima lição é CSS no React. Ela parte do mesmo layout com cabeçalho, main, rodapé e Outlet para mostrar como estilizar cada tela sem deixar as regras vazarem para as demais.

  • react
  • react router
  • rotas
  • spa
  • navegacao

Perguntas frequentes

React Router vem junto com o React?
Não. O React não tem roteamento embutido, e é por isso que existe uma biblioteca separada. Você instala com npm install react-router. O pacote react-router-dom ainda existe e só reexporta o react-router, mas parou na 7.18.2: desde a versão 7 tudo que o navegador precisa já sai do pacote react-router, e é dele que você deve importar.
Qual a diferença entre createBrowserRouter e createHashRouter?
O createBrowserRouter usa URLs limpas, como /livros/12, e por isso exige que o servidor devolva o index.html em qualquer caminho. O createHashRouter põe tudo depois de um # (/#/livros/12), que o servidor nunca chega a ver — serve para hospedagem que você não consegue configurar.
Dá para ter mais de um Outlet no mesmo layout?
Dá para renderizar mais de um Outlet, mas todos mostram a mesma rota filha que combinou com a URL. O React Router não oferece outlets nomeados e independentes. Para preencher áreas diferentes, aninhe outro nível de rota ou componha o conteúdo por props no próprio layout.
Preciso do React Router para uma página só?
Não. Se a aplicação tem uma tela, roteamento é peso morto. Ele passa a valer quando você precisa que uma parte da tela tenha URL própria — para compartilhar o link, para o botão voltar funcionar e para a pessoa recarregar sem perder onde estava.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0 · React 19.2.8 · React Router 7.18.2 · Vite 8.2.2 · Caddy 2.10.2, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React Router — Changelog — reactrouter.com
  2. React Router — Picking a Mode — reactrouter.com
  3. React Router — useNavigate — reactrouter.com
  4. MDN — History API — developer.mozilla.org
  5. Caddy — try_files — caddyserver.com
  6. Google Search — JavaScript SEO e soft 404 — developers.google.com

Continue por aqui