File-Based Routing
Film Fanatic uses TanStack Router with file-based routing, providing:- Type-safe route parameters
- Automatic route tree generation
- Built-in data loading
- SSR support
- Viewport-based prefetching
Route Structure
src/routes/
├── __root.tsx # Root layout with providers
├── index.tsx # Homepage (/)
├── watchlist.tsx # Watchlist page (/watchlist)
├── search.tsx # Search page (/search)
├── disclaimer.tsx # Disclaimer page (/disclaimer)
├── person.$id.tsx # Person details (/person/:id)
├── keyword.$id.tsx # Keyword page (/keyword/:id)
├── collection.$id.{-$slug}.tsx # Collection page (/collection/:id/:slug)
├── list.$type.$slug.tsx # List page (/list/:type/:slug)
├── movie/$id/{-$slug}/ # Movie routes
│ ├── index.tsx # Movie details (/movie/:id/:slug)
│ ├── cast-crew.tsx # Cast & crew (/movie/:id/:slug/cast-crew)
│ └── media.tsx # Media (/movie/:id/:slug/media)
└── tv/$id/{-$slug}/ # TV show routes
├── index.tsx # TV details (/tv/:id/:slug)
├── cast-crew.tsx # Cast & crew (/tv/:id/:slug/cast-crew)
├── media.tsx # Media (/tv/:id/:slug/media)
├── seasons.tsx # All seasons (/tv/:id/:slug/seasons)
└── season.$seasonNumber.tsx # Season details (/tv/:id/:slug/season/:number)
Root Route
The root route provides the shell for all pages:import type { QueryClient } from "@tanstack/react-query";
import {
createRootRouteWithContext,
HeadContent,
Scripts,
} from "@tanstack/react-router";
import { Footer } from "@/components/footer";
import { Navbar } from "@/components/navbar";
import { ThemeProvider } from "@/components/theme-provider";
import { UserSync } from "@/components/user-sync";
import { SITE_CONFIG } from "@/constants";
import { MetaImageTagsGenerator } from "@/lib/meta-image-tags";
import appCss from "@/styles.css?url";
interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
...MetaImageTagsGenerator({
title: SITE_CONFIG.name,
description: SITE_CONFIG.description,
ogImage: SITE_CONFIG.defaultMetaImage,
url: SITE_CONFIG.url,
}),
{ name: "robots", content: "noindex, nofollow" },
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary_large_image" },
],
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", href: "/logo.svg", sizes: "any", type: "image/x-icon" },
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body className="min-h-screen leading-relaxed antialiased">
<ThemeProvider>
<UserSync />
<Navbar />
{children}
<Footer />
<Scripts />
</ThemeProvider>
</body>
</html>
);
}
Router Configuration
import { ClerkProvider, useAuth } from "@clerk/clerk-react";
import { createRouter } from "@tanstack/react-router";
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { DefaultLoader } from "@/components/default-loader";
import { DefaultNotFoundComponent } from "@/components/default-not-found";
import * as TanstackQuery from "@/lib/query/root-provider";
import { routeTree } from "@/routeTree.gen";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
export const getRouter = () => {
const rqContext = TanstackQuery.getContext();
const router = createRouter({
routeTree,
context: { ...rqContext },
defaultPreload: "viewport", // Prefetch routes when they enter viewport
Wrap: (props: { children: React.ReactNode }) => {
return (
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
<TanstackQuery.Provider {...rqContext}>
{props.children}
</TanstackQuery.Provider>
</ConvexProviderWithClerk>
</ClerkProvider>
);
},
scrollRestoration: true,
caseSensitive: true,
defaultStaleTime: 30 * 1000,
defaultPendingComponent: () => <DefaultLoader />,
defaultNotFoundComponent: () => <DefaultNotFoundComponent />,
defaultErrorComponent: () => <DefaultNotFoundComponent />,
});
setupRouterSsrQueryIntegration({
router,
queryClient: rqContext.queryClient,
handleRedirects: true,
wrapQueryClient: true,
});
return router;
};
Page Routes
Homepage
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
import {
PopularMovies,
TrendingDayMovies,
UpcomingMovies,
} from "@/components/homepage-media";
import { SearchBar, SearchBarSkeleton } from "@/components/ui/search-bar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export const Route = createFileRoute("/")({
component: HomePage,
});
function HomePage() {
return (
<section className="flex flex-col items-center justify-center">
<div className="mx-auto max-w-screen-lg px-4 py-10 text-center">
<h1 className="font-black text-2xl sm:text-5xl md:text-[4rem]">
Welcome to<span className="px-2 text-blue-500">Film Fanatic</span>
</h1>
<p className="mb-3 text-xs text-accent-foreground">
Millions of movies, TV shows, and people to discover.
</p>
<Suspense fallback={<SearchBarSkeleton />}>
<SearchBar />
</Suspense>
</div>
<div className="mx-auto flex w-full max-w-screen-xl px-5 py-5">
<div className="flex w-full flex-col gap-10">
<Tabs defaultValue="trending_day">
<div className="flex items-center gap-5">
<h2 className="font-medium text-xl md:text-2xl">Trending</h2>
<TabsList>
<TabsTrigger value="trending_day">Today</TabsTrigger>
<TabsTrigger value="trending_week">This Week</TabsTrigger>
</TabsList>
</div>
<TabsContent value="trending_day">
<TrendingDayMovies />
</TabsContent>
</Tabs>
<section>
<h2 className="font-medium text-xl md:text-2xl">Upcoming Movies</h2>
<UpcomingMovies />
</section>
</div>
</div>
</section>
);
}
Navigation
Type-Safe Links
import { Link } from "@tanstack/react-router";
// Simple route
<Link to="/watchlist">My Watchlist</Link>
// Route with params (fully type-safe)
<Link
to="/movie/$id/$slug"
params={{ id: "123", slug: "inception" }}
>
Inception
</Link>
// Route with search params
<Link
to="/search"
search={{ query: "matrix", page: 1 }}
>
Search Results
</Link>
// Person route
<Link
to="/person/$id"
params={{ id: String(person.id) }}
>
{person.name}
</Link>
Route Parameters
Dynamic Routes
$id- Required parameter (e.g.,/movie/$id){-$slug}- Optional parameter (e.g.,/movie/$id/{-$slug})$type.$slug- Multiple parameters (e.g.,/list/$type/$slug)
URL Structure Examples
/movie/550/fight-club → movie details
/tv/1396/breaking-bad → TV show details
/tv/1396/breaking-bad/seasons → all seasons
/tv/1396/breaking-bad/season.1 → season 1
/person/287 → person details
/collection/131295/the-matrix-collection → collection
/list/movies/popular → movie list
/search?query=matrix → search results
Route Features
Viewport Prefetching
Routes automatically prefetch when links enter the viewport:const router = createRouter({
defaultPreload: "viewport", // Prefetch on viewport entry
});
Scroll Restoration
Scroll position is automatically restored on navigation:const router = createRouter({
scrollRestoration: true,
});
Loading States
Default pending component shows during route transitions:const router = createRouter({
defaultPendingComponent: () => <DefaultLoader />,
});
Error Handling
Default error component handles route errors:const router = createRouter({
defaultErrorComponent: () => <DefaultNotFoundComponent />,
defaultNotFoundComponent: () => <DefaultNotFoundComponent />,
});
Generated Route Tree
TanStack Router auto-generates type-safe route definitions:// Auto-generated in src/routeTree.gen.ts
import { Route as rootRoute } from "./routes/__root";
import { Route as IndexRoute } from "./routes/index";
import { Route as MovieIdSlugRoute } from "./routes/movie/$id/{-$slug}/index";
const routeTree = rootRoute.addChildren([
IndexRoute,
MovieIdSlugRoute,
// ... all other routes
]);
export { routeTree };
