Capítulo 3 de 24
Builds past the quickstart into common real-world routing needs: nested routes with a loader (/posts/$postId), a pathless layout route (_pathlessLayout) that wraps children without adding to the URL, dynamic params, error components, and a root-level not-found component. This is the reference example for "how do I structure a real file-based app."
@tanstack/react-router, @tanstack/router-plugin, @tanstack/react-router-devtools, redaxios (data fetching), zodsrc/routes/__root.tsx defines the shared layout, nav links, and a notFoundComponent. src/routes/_pathlessLayout.tsx (leading underscore) is a layout-only route with no URL segment of its own. src/routes/posts.$postId.tsx uses the $paramName filename convention for a dynamic segment and a loader to fetch data before render.// src/routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => fetchPost(postId),
errorComponent: PostErrorComponent,
notFoundComponent: () => <p>Post not found</p>,
component: PostComponent,
})
export function PostErrorComponent({ error }: ErrorComponentProps) {
return <ErrorComponent error={error} />
}
function PostComponent() {
const post = Route.useLoaderData()
return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{post.title}</h4>
<div className="text-sm">{post.body}</div>
</div>
)
}
// src/routes/_pathlessLayout.tsx
export const Route = createFileRoute('/_pathlessLayout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div className="p-2">
<div className="border-b">I'm a pathless layout</div>
<div><Outlet /></div>
</div>
)
}
$postId in a filename becomes params.postId in the loader; Route.useLoaderData() reads what the loader returned with full type inference; a leading-underscore filename (_pathlessLayout) creates a layout route that groups children under a shared UI without adding a path segment.loader, errorComponent, and notFoundComponent on the route definition itself, TanStack Router calls them automatically during navigation, no manual try/catch in the component._layoutName file to share UI (e.g. a sidebar) across a group of routes when you don't want that grouping to appear in the URL.Route.useLoaderData() is the idiomatic way to read loader output; it's tied to that specific route's generated types, so renaming a param name updates the type everywhere.createRoute + .lazy() code splitting instead of files.$postId pattern to server-side rendering; worth checking when a loader needs to run on the server.