Skip to content

Latest commit

 

History

History

README.md

File-Based Routing with TanStack Router

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.

Key Learning Points

  • 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.ts automatically
  • 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

File Structure and Route Mapping

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

Code Examples

Root Layout Route

// src/routes/__root.tsx:1-17
export const Route = createRootRoute({
  component: () => (
    <AppLayout>
      <Outlet />
    </AppLayout>
  ),
  context: () => ({
    queryClient,
  }),
});

Index Route with Loader

// 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...
}

Dynamic Route with Deferred Data Loading

// 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,
});

Deferred Data with Suspense and Skeletons

// 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>
  );
}

Skeleton Loading Component

// 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>
  );
}

Generated Route Tree

// 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,
]);

Router Setup with Generated Tree

// 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,
    },
  });
};

File-Based Route Conventions

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

Benefits of File-Based Routing

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

Comparison with Code-Based Routing (1-7)

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.