DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

TanStack React Router

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 })
Enter fullscreen mode Exit fullscreen mode

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} />
)
Enter fullscreen mode Exit fullscreen mode

Outlet e layouts aninhados

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

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

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' } })
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>,
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
sgaggjhkjh profile image
sgaggjhkjh •

Really solid walkthrough. The part that finally clicked for me is how the file-based conventions replace the manual getParentRoute wiring — the plugin resolving the tree from the filesystem removes a whole class of mistakes I have made hand-maintaining nested routes in the past.

Also the Register trick for end-to-end type safety is something I had not seen explained this clearly before. Having Link params and search checked at compile time is a big upgrade over finding out at runtime that a param was missing.

One question: with defaultPreload set to 'intent' plus loaders that hit a real API, do you throttle or dedupe the prefetch on hover for users on slow connections, or is that mostly a non-issue in practice?