Introdução

É um roteador para React focado em type-safety de ponta a ponta: rotas, parâmetros de URL, query strings e dados carregados por loaders são todos inferidos pelo TypeScript, sem precisar escrever tipos manualmente. Ele também trata data loading como parte do roteamento (parecido com Remix/Next App Router), não como algo à parte (diferente do React Router clássico).

Duas formas de declarar rotas

Code-based (tudo em um arquivo, manual)

import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router' const rootRoute = createRootRoute({ component: RootLayout }) const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: HomePage, }) const postRoute = createRoute({ getParentRoute: () => rootRoute, path: '/posts/$postId', component: PostPage, }) const routeTree = rootRoute.addChildren([indexRoute, postRoute]) const router = createRouter({ routeTree }) 

File-based (recomendado)

a estrutura de pastas/arquivos em src/routes define a árvore, e um plugin (Vite/Webpack/etc.) gera o routeTree.gen.ts automaticamente. Cada arquivo usa createFileRoute('/caminho')({...}) e nunca precisa declarar getParentRoute — o plugin resolve isso pela posição no filesystem.

Convenções de nome de arquivo:

Arquivo Vira rota
routes/__root.tsx layout raiz (obrigatório)
routes/index.tsx /
routes/about.tsx /about
routes/posts/index.tsx /posts/
routes/posts/$postId.tsx /posts/:postId (param dinâmico)
routes/posts/$postId.edit.tsx /posts/:postId/edit
routes/_authenticated.tsx + pasta routes/_authenticated/ layout sem adicionar segmento na URL (pathless layout)
routes/posts/-components/Card.tsx ignorado — prefixo - é só código auxiliar, não vira rota
routes/posts.lazy.tsx parte da rota com code-splitting automático

O router e o RouterProvider

O Register é o truque que faz o TypeScript "conhecer" todas as rotas do app em qualquer componente, sem precisar importar nada específico.

import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree }) declare module '@tanstack/react-router' { interface Register { router: typeof router // isso dá type-safety global pra Link, useNavigate, etc. } } createRoot(document.getElementById('root')!).render( <RouterProvider router={router} /> ) 

Outlet e layouts aninhados

Toda rota com filhas renderiza um <Outlet /> onde a rota filha deve aparecer:

export const Route = createRootRoute({ component: () => ( <> <NavBar /> <Outlet /> </> ), }) 

Isso permite layouts aninhados: _authenticated.tsx pode renderizar uma sidebar + <Outlet />, e todas as rotas dentro de _authenticated/ herdam esse layout.

Navegação

import { Link, useNavigate } from '@tanstack/react-router' <Link to="/posts/$postId" params={{ postId: '123' }}>Ver post</Link>  const navigate = useNavigate() navigate({ to: '/posts/$postId', params: { postId: '123' } }) 

Se a rota não existir ou o param faltar, é erro de tipo em tempo de compilação — não em runtime.

Params dinâmicos

// routes/posts/$postId.tsx export const Route = createFileRoute('/posts/$postId')({ component: PostPage, }) function PostPage() { const { postId } = Route.useParams() // tipado como string } 

Search params (query string) validados

Diferente do React Router, aqui a query string é tipada e validada (geralmente com Zod):

import { z } from 'zod' const searchSchema = z.object({ page: z.number().catch(1), filter: z.string().optional(), }) export const Route = createFileRoute('/posts/')({ validateSearch: searchSchema, component: PostsList, }) function PostsList() { const { page, filter } = Route.useSearch() // já validado e tipado } 

E <Link search={{ page: 2 }}> também é tipado contra esse schema.

8. Loaders (carregar dados antes de renderizar)

export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => fetchPost(params.postId), component: PostPage, }) function PostPage() { const post = Route.useLoaderData() // tipo inferido do retorno do loader } 

Loaders rodam antes do componente montar (e podem rodar em paralelo com o parent). Combinado com defaultPreload: 'intent' no createRouter, os dados já começam a carregar no hover/foco de um <Link>, antes mesmo do clique.

Estados de pending / erro / not-found

export const Route = createFileRoute('/posts/$postId')({ loader: ..., pendingComponent: () => <Spinner />, // enquanto o loader roda errorComponent: ({ error, reset }) => ..., // se o loader ou render lançar erro notFoundComponent: () => <p>Não encontrado</p>, }) 

Esses fallbacks seguem a hierarquia de rotas: se uma rota não define o seu, o pai assume (até chegar no __root.tsx).

Contexto de rota

beforeLoad pode popular um contexto tipado compartilhado por toda a árvore (útil pra auth, QueryClient, etc.):

export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({...}) // em uma rota filha: loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery) 

Devtools

@tanstack/react-router-devtools dá um painel visual da árvore de rotas, matches ativos e estado dos loaders — ótimo pra depurar durante o desenvolvimento.