Fumadocs + Code Hike
Frontend

TanStack Router

Router type-safe moderno para React com nested routing e code splitting

O que é TanStack Router?

TanStack Router é um roteador type-safe para React que oferece nested routing, code splitting automático, search params tipados e excelente performance. Desenvolvido pela mesma equipe do TanStack Query.

Por que utilizamos TanStack Router na IngenioLab?

  • Type Safety completa: Rotas, params e search tipados
  • Nested routing: Layouts aninhados naturalmente
  • Code splitting automático: Lazy loading integrado
  • Performance: Otimizações automáticas
  • DevTools: Debugging avançado
  • File-based routing: Organização baseada em arquivos

Instalação

npm install @tanstack/react-router
npm install -D @tanstack/router-vite-plugin @tanstack/router-devtools

Configuração Base

1. Vite Plugin:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
export default defineConfig({
plugins: [
react(),
TanStackRouterVite()
]
})

2. Estrutura de rotas:

src/routes/
├── __root.tsx # Layout raiz
├── index.tsx # Página inicial (/)
├── about.tsx # /about
├── users/
│ ├── index.tsx # /users
│ └── $userId.tsx # /users/$userId
└── dashboard/
├── _layout.tsx # Layout para /dashboard/*
├── index.tsx # /dashboard
└── settings.tsx # /dashboard/settings

3. Root Route:

// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<div className="p-2 flex gap-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
<Link to="/users" className="[&.active]:font-bold">
Users
</Link>
</div>
<hr />
<Outlet />
<TanStackRouterDevtools />
</>
)
})

4. Configuração principal:

// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
}

Rotas Básicas

1. Página simples:

// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: About
})
function About() {
return (
<div className="p-2">
<h3>About</h3>
<p>Sobre a IngenioLab</p>
</div>
)
}

2. Rota com parâmetros:

// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useUser } from '@/hooks/useUsers'
export const Route = createFileRoute('/users/$userId')({
component: UserDetail
})
function UserDetail() {
const { userId } = Route.useParams()
const { data: user, isLoading } = useUser(userId)
if (isLoading) return <div>Loading...</div>
return (
<div className="p-2">
<h3>User: {user?.name}</h3>
<p>Email: {user?.email}</p>
</div>
)
}

3. Rota com Search Params:

// src/routes/users/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const usersSearchSchema = z.object({
page: z.number().catch(1),
search: z.string().optional(),
role: z.enum(['admin', 'user']).optional()
})
export const Route = createFileRoute('/users/')({
component: Users,
validateSearch: usersSearchSchema
})
function Users() {
const { page, search, role } = Route.useSearch()
const navigate = Route.useNavigate()
const updateSearch = (newSearch: Partial<typeof search>) => {
navigate({
search: (prev) => ({ ...prev, ...newSearch })
})
}
return (
<div>
<input
value={search || ''}
onChange={(e) => updateSearch({ search: e.target.value })}
placeholder="Search users..."
/>
<select
value={role || ''}
onChange={(e) => updateSearch({ role: e.target.value as any })}
>
<option value="">All roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
{/* Lista de usuários baseada nos filtros */}
</div>
)
}

Layouts Aninhados

1. Layout Dashboard:

// src/routes/dashboard/_layout.tsx
import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard/_layout')({
component: DashboardLayout
})
function DashboardLayout() {
return (
<div className="dashboard-layout">
<aside className="sidebar">
<nav>
<Link to="/dashboard" className="[&.active]:font-bold">
Overview
</Link>
<Link to="/dashboard/settings" className="[&.active]:font-bold">
Settings
</Link>
</nav>
</aside>
<main className="main-content">
<Outlet />
</main>
</div>
)
}

2. Páginas do Dashboard:

// src/routes/dashboard/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard/')({
component: Dashboard
})
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<p>Overview do sistema</p>
</div>
)
}

Data Loading

1. Loader com TanStack Query:

// src/routes/users/$userId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
import { queryOptions } from '@tanstack/react-query'
import { userService } from '@/services/userService'
const userQueryOptions = (userId: string) =>
queryOptions({
queryKey: ['users', userId],
queryFn: () => userService.getById(userId)
})
export const Route = createFileRoute('/users/$userId')({
component: UserDetail,
loader: ({ context: { queryClient }, params: { userId } }) =>
queryClient.ensureQueryData(userQueryOptions(userId)),
errorComponent: ({ error }) => <div>Error: {error.message}</div>,
notFoundComponent: () => <div>User not found</div>
})
function UserDetail() {
const { userId } = Route.useParams()
const user = Route.useLoaderData()
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}

2. Configurar Query Client:

// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: {
queryClient
}
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
)
}

Autenticação e Guards

1. Auth Context:

// src/auth.tsx
import { createContext, useContext } from 'react'
interface AuthContext {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
const authContext = createContext<AuthContext | null>(null)
export function useAuth() {
const context = useContext(authContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}

2. Route Guards:

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useAuth } from '@/auth'
export const Route = createFileRoute('/_authenticated')({
component: AuthenticatedLayout,
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href
}
})
}
}
})
function AuthenticatedLayout() {
return <Outlet />
}

3. Rotas protegidas:

// src/routes/_authenticated/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/dashboard')({
component: Dashboard
})
function Dashboard() {
const { user } = useAuth()
return (
<div>
<h1>Welcome, {user?.name}</h1>
</div>
)
}

1. Programmatic Navigation:

// src/components/UserActions.tsx
import { useRouter, useNavigate } from '@tanstack/react-router'
export const UserActions = ({ userId }: { userId: string }) => {
const router = useRouter()
const navigate = useNavigate()
const goToUser = () => {
navigate({
to: '/users/$userId',
params: { userId }
})
}
const goToEdit = () => {
navigate({
to: '/users/$userId/edit',
params: { userId },
search: { tab: 'profile' }
})
}
const goBack = () => {
router.history.back()
}
return (
<div>
<button onClick={goToUser}>View User</button>
<button onClick={goToEdit}>Edit User</button>
<button onClick={goBack}>Go Back</button>
</div>
)
}
// src/components/Navigation.tsx
import { Link } from '@tanstack/react-router'
export const Navigation = () => {
return (
<nav>
<Link
to="/users/$userId"
params={{ userId: '123' }}
activeOptions={{ exact: true }}
className="[&.active]:font-bold"
>
User Detail
</Link>
<Link
to="/users"
search={{ page: 1, role: 'admin' }}
className="[&.active]:font-bold"
>
Admin Users
</Link>
</nav>
)
}

Code Splitting

1. Lazy Loading automático:

// src/routes/heavy-page.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/heavy-page')({
component: HeavyPage
})
function HeavyPage() {
return (
<div>
<h1>Heavy Page</h1>
{/* Componente pesado que será carregado sob demanda */}
</div>
)
}

2. Loading States:

// src/routes/__root.tsx
export const Route = createRootRoute({
component: RootComponent,
pendingComponent: () => <div>Loading...</div>,
errorComponent: ({ error }) => (
<div>Something went wrong: {error.message}</div>
)
})

Formulários e Actions

1. Form Actions:

// src/routes/users/create.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { userService } from '@/services/userService'
export const Route = createFileRoute('/users/create')({
component: CreateUser,
action: async ({ request }) => {
const formData = await request.formData()
const userData = Object.fromEntries(formData)
const newUser = await userService.create(userData)
throw redirect({
to: '/users/$userId',
params: { userId: newUser.id }
})
}
})
function CreateUser() {
return (
<form method="post">
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
</form>
)
}

Error Handling

1. Error Boundaries por rota:

// src/routes/users.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/users')({
component: Users,
errorComponent: ({ error, reset }) => (
<div>
<h2>Error loading users</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
})

2. Not Found personalizado:

// src/routes/users/$userId.tsx
export const Route = createFileRoute('/users/$userId')({
component: UserDetail,
notFoundComponent: () => (
<div>
<h2>User not found</h2>
<Link to="/users">Back to users</Link>
</div>
)
})

TypeScript Integration

1. Tipos gerados automaticamente:

// Os tipos são gerados automaticamente pelo plugin
// Não precisa definir manualmente
// Uso com type safety completa
const navigate = useNavigate()
navigate({
to: '/users/$userId', // Autocomplete e verificação
params: { userId: '123' }, // Tipado
search: { page: 1 } // Tipado baseado no schema
})

2. Custom Router Types:

// src/router.types.ts
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
interface StaticDataRouteOption {
title?: string
description?: string
}
}

DevTools e Debug

1. Router DevTools:

// src/routes/__root.tsx
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
{process.env.NODE_ENV === 'development' && (
<TanStackRouterDevtools />
)}
</>
)
})