This example demonstrates TanStack Router's file-based routing system, where routes are automatically generated from the file structure in the src/routes/ directory, combined with route loaders for optimized data fetching.
- File-Based Routing: Routes defined by file structure instead of programmatic configuration
- Automatic Route Generation: Router tree generated from
src/routes/directory structure - Route File Naming: Special naming conventions for dynamic routes and nested layouts
- Generated Route Tree: TanStack Router CLI generates
routeTree.gen.tsautomatically - Co-located Route Logic: Each route file contains component, loader, and configuration
- Route Loaders with File-Based Routes: Same data loading patterns but organized by file structure
src/routes/
├── __root.tsx → Root layout component
├── index.tsx → "/" route
├── posts.$id.tsx → "/posts/[id]" dynamic route
├── users.tsx → "/users" route
└── users.$id.tsx → "/users/[id]" dynamic route
// src/routes/__root.tsx:1-17
export const Route = createRootRoute({
component: () => (
<AppLayout>
<Outlet />
</AppLayout>
),
context: () => ({
queryClient,
}),
});// src/routes/index.tsx:7-14
export const Route = createFileRoute("/")({
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData(postsQueryOptions),
component: Index,
});
function Index() {
const { data: posts } = useSuspenseQuery(postsQueryOptions);
// Component code...
}// src/routes/posts.$id.tsx:9-21
export const Route = createFileRoute("/posts/$id")({
loader: async ({ context: { queryClient }, params: { id } }) => {
// First load the post
const post = await queryClient.ensureQueryData(postQueryOptions(id));
// Load user immediately
await queryClient.ensureQueryData(userQueryOptions(post.userId));
// Defer comments loading - return the promise without awaiting
const deferredComments = queryClient.ensureQueryData(
postCommentsQueryOptions(id),
);
return {
deferredComments,
};
},
component: PostPage,
});// src/routes/posts.$id.tsx:243-284
function PostPage() {
const queryClient = useQueryClient();
const { id: postId } = useParams({ from: "/posts/$id" });
const { deferredComments } = Route.useLoaderData();
// Post and user data is already loaded by the route loader, so these will resolve immediately
const { data: post } = useSuspenseQuery(postQueryOptions(postId));
const { data: user } = useSuspenseQuery(userQueryOptions(post.userId));
return (
<Stack>
{/* Post content renders immediately */}
<Box>
<Title order={1}>Post: {post.id}</Title>
<Title order={2}>{post.title}</Title>
<Text my="lg">{post.body}</Text>
</Box>
{/* Comments load asynchronously with skeleton loading state */}
<Suspense fallback={<CommentsSkeleton />}>
<Await promise={deferredComments}>
{() => <CommentsSection postId={postId} queryClient={queryClient} />}
</Await>
</Suspense>
</Stack>
);
}// src/routes/posts.$id.tsx:60-74
function CommentsSkeleton() {
return (
<Stack gap="xl">
{Array.from({ length: 3 }).map((_, index) => (
<Card withBorder key={index}>
<Stack gap="xs">
<Skeleton height={20} width="30%" />
<Skeleton height={16} width="50%" />
<Skeleton height={60} />
</Stack>
</Card>
))}
</Stack>
);
}// src/routeTree.gen.ts - Auto-generated by TanStack Router CLI
import { Route as rootRoute } from "./routes/__root";
import { Route as UsersIdRoute } from "./routes/users.$id";
import { Route as UsersRoute } from "./routes/users";
import { Route as PostsIdRoute } from "./routes/posts.$id";
import { Route as IndexRoute } from "./routes/index";
// Route tree structure is automatically inferred from file system
const routeTree = rootRoute.addChildren([
UsersIdRoute,
UsersRoute,
PostsIdRoute,
IndexRoute,
]);// src/router.tsx:4-16
import { routeTree } from "./routeTree.gen";
export const createAppRouter = (queryClient: QueryClient) => {
return createRouter({
routeTree, // Uses generated route tree
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
scrollRestoration: true,
context: {
queryClient,
},
});
};Root Route: __root.tsx - The top-level layout component
Index Routes: index.tsx - Matches the parent route exactly
Dynamic Routes: $param.tsx - Creates [param] parameter
Nested Routes: parent.child.tsx - Creates nested route structure
Layout Routes: Directory with index.tsx creates layout + children
1. Intuitive Organization
- Route structure matches URL structure
- Easy to find and organize route-related code
- No separate route configuration to maintain
2. Co-located Logic
- Component, loader, and route config in same file
- Related code stays together
- Easier to maintain and refactor
- Deferred loading for progressive data streaming
3. Automatic Route Generation
- No manual route tree configuration
- CLI automatically generates route tree from file structure
- Reduces boilerplate and potential errors
4. Type Safety Maintained
- Full TypeScript support with generated types
- Parameter types inferred from file names
- Same type safety as code-based routing
Code-Based (1-7):
// All routes defined in single AppRoutes.tsx file
const postRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/posts/$id",
loader: async ({ context, params }) => {
/* ... */
},
component: PostPage,
});File-Based (1-8):
// Each route in its own file: src/routes/posts.$id.tsx
export const Route = createFileRoute("/posts/$id")({
loader: async ({ context, params }) => {
/* ... */
},
component: PostPage,
});File-based routing provides the same powerful data loading capabilities as code-based routing, but with better organization and developer experience through conventional file structure.