Routing
O módulo routing do tempest-react-sdk embrulha o React Router em modo declarativo (^7 || ^8) e devolve uma superfície única de import: você declara sua árvore de rotas com defineRoutes, monta tudo com um único <AppRouter> e importa cada primitivo (Link, Outlet, useNavigate, …) direto do SDK. Em cima disso, o SDK adiciona o que todo app Tempest repete na mão: code-splitting com retry automático em chunk velho, guards declarativos de rota e um <Suspense> já pronto. Esta página te leva do zero a uma árvore com layout aninhado, lazy loading e rotas protegidas.
react-router é peer dependency — instale no seu app
npm install react-router
O SDK não empacota o react-router junto, porque ele guarda contexto React: uma cópia aninhada em tempest-react-sdk/node_modules seria um <Router> diferente do seu, e todo hook do SDK estouraria com useNavigate() may be used only in the context of a <Router>. O range aceito é ^7 || ^8 — a superfície re-exportada aqui é idêntica nos dois majors, e nenhum deles usa react-router-dom (os bindings de DOM vêm no próprio react-router).
Por que o SDK é dono do roteamento agora
Antes, cada app criava seu próprio <Suspense>, escrevia o seu helper de guard e reinventava o retry de chunk. Isso gerava divergência entre os apps Tempest e import paths espalhados.
Com o módulo routing você ganha:
- Uma só superfície de import. Tudo vem de
"tempest-react-sdk"— componentes, hooks e os primitivos re-exportados do React Router. Seu app instala oreact-router(peer), mas importa de um lugar só. - Declarativo. Você descreve o que são as rotas (uma árvore de objetos), não como montá-las imperativamente.
- Baterias inclusas.
<AppRouter>já constrói o router, o<Suspense>e as<Routes>.defineRouteste dá tipagem. Guards e lazy loading são campos da própria rota.
Primitivos re-exportados
O SDK re-exporta os primitivos declarativos do React Router para você importar tudo do mesmo lugar: BrowserRouter, HashRouter, MemoryRouter, Routes, Route, Outlet, Navigate, Link, NavLink, useNavigate, useParams, useSearchParams, useLocation, useMatch, useRouteError e redirect.
Importar direto de react-router também funciona e resolve para a mesma cópia — o re-export existe por conveniência, não por isolamento. Isso torna a adoção incremental possível: um app que já usa react-router direto pode passar a usar o SDK sem reescrever import nenhum.
Construindo a árvore com defineRoutes
defineRoutes é um helper de identidade: ele recebe um array de TempestRouteObject e devolve o mesmo array, mas com tipagem completa. Você ganha autocomplete e checagem de tipos na árvore sem precisar anotar nada na mão.
// routes.tsx
import { defineRoutes } from "tempest-react-sdk";
import { Home } from "@/pages/Home";
import { Login } from "@/pages/Login";
export const routes = defineRoutes([
{
path: "/",
element: <Home />,
},
{
path: "login",
element: <Login />,
},
]);
Cada TempestRouteObject aceita:
| Campo | Tipo | Descrição |
|---|---|---|
path |
string |
Segmento de URL da rota. |
index |
boolean |
Rota índice do pai. Mutuamente exclusivo com path. |
element |
ReactNode |
O que renderizar quando a rota casa. |
lazy |
() => Promise<{ default: ComponentType }> |
Carrega o componente sob demanda (code-split). Retry automático em chunk velho. |
children |
TempestRouteObject[] |
Rotas aninhadas. |
guard |
boolean \| (() => boolean) |
Quando falsy, renderiza um redirect no lugar do element. |
redirectTo |
string |
Destino do redirect do guard. Padrão "/". |
caseSensitive |
boolean |
Faz o match de path diferenciar maiúsculas/minúsculas. |
Depois é só passar a árvore para o <AppRouter>:
// App.tsx
import { AppRouter } from "tempest-react-sdk";
import { routes } from "@/routes";
export function App() {
return <AppRouter routes={routes} />;
}
O <AppRouter> monta sozinho o router, o <Suspense> e as <Routes> a partir da árvore. Props disponíveis:
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
routes |
TempestRouteObject[] |
— | A árvore de rotas (obrigatória). |
router |
"browser" \| "hash" \| "memory" |
"browser" |
Qual tipo de router usar. |
basename |
string |
— | Prefixo de path comum a todas as rotas. |
initialEntries |
string[] |
— | Histórico inicial — só para o router "memory". |
fallback |
ReactNode |
— | Fallback do <Suspense> enquanto um chunk lazy carrega. |
index vs path
Toda rota com children precisa decidir o que mostrar quando a URL casa exatamente com o pai. Essa é a rota index: ela não tem path próprio, apenas marca index: true.
// routes.tsx
import { defineRoutes } from "tempest-react-sdk";
import { RootLayout } from "@/layouts/RootLayout";
import { Home } from "@/pages/Home";
import { About } from "@/pages/About";
export const routes = defineRoutes([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{ path: "about", element: <About /> },
],
},
]);
Aqui, abrir / renderiza o <RootLayout> com o <Home> dentro; abrir /about renderiza o <RootLayout> com o <About> dentro.
index e path são mutuamente exclusivos
Uma rota é índice (index: true) ou tem path, nunca os dois. Marcar os dois é um erro de configuração.
Layouts aninhados com Outlet
A rota pai renderiza o layout; os filhos renderizam dentro dele através do <Outlet>. O <Outlet> é o ponto onde o React Router injeta a rota filha que casou.
// RootLayout.tsx
import { Link, Outlet } from "tempest-react-sdk";
export function RootLayout() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<Outlet />
</div>
);
}
A <nav> fica visível em todas as rotas filhas; o <Outlet> troca de conteúdo conforme a URL. Use <Link> (também re-exportado pelo SDK) para navegar sem recarregar a página.
Navegação programática
Para navegar a partir de código (depois de um submit, por exemplo), use useNavigate: const navigate = useNavigate(); navigate("/dashboard");.
Lazy loading + o fallback do Suspense
Páginas pesadas não precisam entrar no bundle inicial. Use o campo lazy para carregar o componente sob demanda. Ele recebe uma função que faz import() dinâmico e devolve o módulo com default.
// routes.tsx
import { defineRoutes } from "tempest-react-sdk";
import { RootLayout } from "@/layouts/RootLayout";
import { Home } from "@/pages/Home";
export const routes = defineRoutes([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{
path: "dashboard",
lazy: () => import("@/pages/Dashboard"),
},
],
},
]);
Como o chunk demora um instante para baixar, o <AppRouter> envolve tudo num <Suspense>. Passe um fallback para mostrar algo enquanto carrega:
// App.tsx
import { AppRouter } from "tempest-react-sdk";
import { routes } from "@/routes";
export function App() {
return <AppRouter routes={routes} fallback={<p>Loading…</p>} />;
}
Retry automático em chunk velho
Quando você faz um novo deploy, os nomes dos chunks mudam. Um usuário com a aba aberta há horas pode pedir um chunk que não existe mais e tomar um erro de import. O lazy do SDK detecta esse caso e tenta recarregar automaticamente — você não precisa escrever esse retry na mão.
Guards: protegendo rotas
Quase todo app tem rotas que só usuários autenticados podem ver. O campo guard resolve isso direto na árvore: quando o valor é falsy, o <AppRouter> renderiza um redirect no lugar do element.
Forma booleana
Se a condição já está disponível como valor, passe um booleano:
// routes.tsx
import { defineRoutes } from "tempest-react-sdk";
import { Dashboard } from "@/pages/Dashboard";
const isAuthenticated = false;
export const routes = defineRoutes([
{
path: "dashboard",
element: <Dashboard />,
guard: isAuthenticated,
redirectTo: "/login",
},
]);
Forma de função (auth store)
Na prática, o estado de auth vive numa store. Passe uma função que lê a store na hora da renderização — assim o guard sempre vê o valor atual:
// routes.tsx
import { defineRoutes } from "tempest-react-sdk";
import { useAuth } from "@/stores/auth"; // store baseada em createAuthStore
import { RootLayout } from "@/layouts/RootLayout";
import { Home } from "@/pages/Home";
import { Login } from "@/pages/Login";
export const routes = defineRoutes([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{ path: "login", element: <Login /> },
{
path: "dashboard",
lazy: () => import("@/pages/Dashboard"),
guard: () => useAuth.getState().isAuthenticated,
redirectTo: "/login",
},
],
},
]);
O guard roda na renderização — leia a store via getState() ou um hook
A função do guard é avaliada durante a renderização da rota. Por isso ela precisa ler o estado naquele momento: use useAuth.getState().isAuthenticated (leitura imperativa, fora do React) ou um hook de seleção dentro de um componente. Não capture o valor uma vez fora da função — você congelaria o estado de auth no carregamento inicial.
Quando guard é falsy, o usuário é redirecionado para redirectTo (padrão "/"). No exemplo acima, quem não está autenticado e tenta abrir /dashboard cai em /login.
RouteGuard standalone
Às vezes você quer proteger uma parte da UI sem que ela seja uma rota — ou prefere o guard explícito no JSX. Para isso existe o <RouteGuard>: ele renderiza os children quando when é verdadeiro, senão emite um <Navigate>.
// ProtectedDashboard.tsx
import { RouteGuard } from "tempest-react-sdk";
import { useAuth } from "@/stores/auth";
import { Dashboard } from "@/pages/Dashboard";
export function ProtectedDashboard() {
const isAuthenticated = useAuth((state) => state.isAuthenticated);
return (
<RouteGuard when={isAuthenticated} redirectTo="/login" replace>
<Dashboard />
</RouteGuard>
);
}
Props do <RouteGuard>:
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
when |
boolean |
— | Renderiza os children quando verdadeiro. |
redirectTo |
string |
"/" |
Destino quando when é falso. |
replace |
boolean |
true |
Substitui a entrada no histórico em vez de empilhar. |
children |
ReactNode |
— | O que proteger. |
Mesma regra de leitura de estado
Aqui você está num componente React, então leia a store com o hook (useAuth((state) => state.isAuthenticated)) para re-renderizar quando o auth mudar — diferente do guard da árvore, que usa getState() por rodar fora do ciclo de hooks.
Escolhendo o tipo de router
O <AppRouter> aceita três tipos via a prop router:
"browser"(padrão) — usa a History API; URLs limpas (/dashboard). É o que você quer em produção."hash"— URLs com#(/#/dashboard). Útil quando o servidor não consegue fazer fallback de todas as rotas para oindex.html."memory"— histórico em memória, sem tocar a URL do navegador. Ideal para testes e ambientes não-DOM.
Em testes, combine "memory" com initialEntries para começar numa rota específica:
// App.test.tsx
import { render, screen } from "@testing-library/react";
import { AppRouter } from "tempest-react-sdk";
import { routes } from "@/routes";
test("renders the dashboard route", () => {
render(
<AppRouter
routes={routes}
router="memory"
initialEntries={["/dashboard"]}
fallback={<p>Loading…</p>}
/>,
);
expect(screen.getByRole("heading", { name: /dashboard/i })).toBeInTheDocument();
});
initialEntries é só do router memory
initialEntries define o histórico inicial e só faz sentido com router="memory". Nos routers browser/hash a rota inicial vem da própria URL do navegador.
Recap
- O módulo
routingembrulha o React Router v8 declarativo e te dá uma superfície única de import — tudo vem de"tempest-react-sdk". defineRoutestipa sua árvore;<AppRouter routes={...} />monta router +<Suspense>+<Routes>sozinho.- Use
index: truepara a rota padrão de um layout epathpara as demais — nunca os dois juntos. - Layouts aninhados renderizam filhos no
<Outlet>; navegue com<Link>. lazyfaz code-split com retry automático em chunk velho; mostre ofallbackdo Suspense enquanto carrega.- Proteja rotas com
guard(booleano ou função) +redirectTo, ou com<RouteGuard when={...}>no JSX. O guard roda na renderização — leia a store viagetState()(na árvore) ou via hook (no componente). - Escolha o router com a prop
router:"browser"em produção,"memory"+initialEntriesem testes.