Tanstack Router for React - A Complete Guide
Tanstack Router provides an easy, safe way to define routes for your React web site, and is worth a look, as this article shows.
Discover how at OpenReplay.com.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.
Tanstack Router is a modern and scalable routing solution for React, created by Tanner Linsey(creator of react-query). Its core objective centers around type-safety and developer productivity.
The core philosophy behind TanStack Router is simply the full utilization of Typescript for web routing. Developers should be able to write type-safe routes, actions, and loaders, which will result in fewer runtime errors. This provides a cohesive environment where routes are well-defined type-safe contracts instead of just navigation pathways in an application.
On the contrary, React Router is built on the philosophy of simplicity and flexibility. It aims to make routing implementation in React applications simple. Unlike TanStack Router, React Router takes an unopinionated and incremental adoption approach, allowing developers to start simple and gradually add more advanced routing techniques as the need arises.
The following table from the TanStack Router documentation compares TanStack Router and React Router:
Feature/Capability Key:
- ✅ 1st-class, built-in, and ready to use with no added configuration or code
- 🔵 Supported via add-on package
- 🟡 Partial Support
- 🔶 Possible, but requires custom code/implementation/casting
- 🛑 Not officially supported
TanStack Router | React Router DOM (Website) | Next.JS (Website) | |
---|---|---|---|
History, Memory & Hash Routers | ✅ | ✅ | 🛑 |
Nested / Layout Routes | ✅ | ✅ | ✅ |
Suspense-like Route Transitions | ✅ | ✅ | ✅ |
Typesafe Routes | ✅ | 🛑 | 🟡 |
Code-based Routes | ✅ | ✅ | 🛑 |
File-based Routes | ✅ | ✅ | ✅ |
Router Loaders | ✅ | ✅ | ✅ |
SWR Loader Caching | ✅ | 🛑 | ✅ |
Route Prefetching | ✅ | ✅ | ✅ |
Auto Route Prefetching | ✅ | 🔵 (via Remix) | ✅ |
Route Prefetching Delay | ✅ | 🔶 | 🛑 |
Path Params | ✅ | ✅ | ✅ |
Typesafe Path Params | ✅ | 🛑 | 🛑 |
Path Param Validation | ✅ | 🛑 | 🛑 |
Custom Path Param Parsing/Serialization | ✅ | 🛑 | 🛑 |
Ranked Routes | ✅ | ✅ | ✅ |
Active Link Customization | ✅ | ✅ | ✅ |
Optimistic UI | ✅ | ✅ | 🔶 |
Typesafe Absolute + Relative Navigation | ✅ | 🛑 | 🛑 |
Route Mount/Transition/Unmount Events | ✅ | 🛑 | 🛑 |
Devtools | ✅ | 🛑 | 🛑 |
Basic Search Params | ✅ | ✅ | ✅ |
Search Param Hooks | ✅ | ✅ | ✅ |
<Link/> /useNavigate Search Param API | ✅ | 🟡 (search-string only via the to /search options) | 🟡 (search-string only via the to /search options) |
JSON Search Params | ✅ | 🔶 | 🔶 |
TypeSafe Search Params | ✅ | 🛑 | 🛑 |
Search Param Schema Validation | ✅ | 🛑 | 🛑 |
Search Param Immutability + Structural Sharing | ✅ | 🔶 | 🛑 |
Custom Search Param parsing/serialization | ✅ | 🔶 | 🛑 |
Search Param Middleware | ✅ | 🛑 | 🛑 |
Suspense Route Elements | ✅ | ✅ | ✅ |
Route Error Elements | ✅ | ✅ | ✅ |
Route Pending Elements | ✅ | ✅ | ✅ |
<Block> /useBlocker | ✅ | 🔶 | ❓ |
SSR | ✅ | ✅ | ✅ |
Streaming SSR | ✅ | ✅ | ✅ |
Deferred Primitives | ✅ | ✅ | ✅ |
Navigation Scroll Restoration | ✅ | ✅ | ❓ |
Loader Caching (SWR + Invalidation) | 🔶 (TanStack Query is recommended) | 🛑 | ✅ |
Actions | 🔶 (TanStack Query is recommended) | ✅ | ✅ |
<Form> API | 🛑 | ✅ | ✅ |
Full-Stack APIs | 🛑 | ✅ | ✅ |
credit: TanStack Router Documentation
Build a Single Page Application with React and TanStack Router
In this section, we will build a single-page application in React that retrieves data from an API and runs in the browser. Routing will be done using TanStack Router, and Tailwind-CSS will be used for styling. The application you’ll build will show information about popular movies via the TMDb API.
Building this project from scratch will help us understand how to use the TanStack Router from a hands-on perspective, which is crucial when learning a new technology.
Prerequisite
- Working knowledge of React and Typescript
- Familiarity with Tailwind CSS
- Code Editor — such as VSCode
- Node.js Latest LTS installed on your computer.
- TMDb API key
The complete code for this project can be found on GitHub.
Project Demo
Creating a Vite App and Installing Dependencies
We will use vite to create our React app.
npm create vite@latest movie-app --template react-ts
npm install
The preceding command allows you to scaffold a react project with Typescript using Vite. We use Typescript because our routing will be done via Tanstack Router, which is 100% type-safe.
Next, install the following dependencies; they will be utilized for the project.
npm install -D zod tailwindcss postcss autoprefixer
npx tailwindcss init -p
In the preceding command, you installed zod, tailwindcss and its peer dependencies. The npx tailwindcss init -p
command generates the tailwind.config.js
and postcss.config.js
files, which will be used to configure tailwind. Also, the zod library will validate and infer Typescript type(s) for routes.
After running the above commands, the following package.json
will be created for the project.
{
"name": "movie-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"zod": "^3.22.4"
}
}
Now, let’s configure tailwind via the tailwind.config.js
and index.css
files, respectively.
Enter the following code:
tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
src\index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Tanstack Router Setup
The development environment for our project is ready. In this section, we will install and configure TanStack Router, which is the main focus of this project. Let’s start by installing the TanStack Router library.
Enter the following commands:
npm install @tanstack/react-router
npm install --save-dev @tanstack/react-router-vite-plugin
The @tanstack/react-router-vite-plugin
will regenerate the routes whenever our application compiles.
Next, we set up Vite to use the plugin we just installed.
Enter the following code in vite.config.ts
:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), TanStackRouterVite()],
});
The Routes Directory
All route implementations for our application are carried out in the’ routes’ directory.
In the source[src
] directory, create a new subdirectory called routes
. This directory will contain all the routes of our application. Tanstack Router will infer routes based on its folder structure via filenames.
Next, inside the routes
directory create the files __root.tsx
and index.tsx
respectively.
📦 movie-app
┣ 📂 src
┃ ┣ 📂 routes
┃ ┣ 📄 __root.tsx
┃ ┣ 📄 index.tsx
Next, run the following command:
npm run dev
The preceding command will generate a routeTree for our application located in routeTree.gen.ts
inside the src
directory.
// src\routeTree.gen.ts
import { Route as rootRoute } from "./routes/__root";
import { Route as IndexImport } from "./routes/index";
// Create/Update Routes
const IndexRoute = IndexImport.update({
path: "/",
getParentRoute: () => rootRoute,
} as any);
// Populate the FileRoutesByPath interface
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
}
}
// Create and export the route tree
export const routeTree = rootRoute.addChildren([IndexRoute]);
Router Instance
The router instance is the mechanism that connects TanStack Router to our React application—just like <BrowserRouter>
from React Router.
Enter the following code in App.tsx
:
// src\App.tsx
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
function App() {
return <RouterProvider router={router} />;
}
export default App;
The preceding code creates a router instance via the createRouter
function from TanStack Router, which ensures all declared routes are 100% type-safe.
Layout Route
The layout route is the parent container of all routes in our application.
Enter the following code in __root.tsx
:
// src\routes\__root.tsx
import { createRootRoute, Outlet, Link } from '@tanstack/react-router';
export const Route = createRootRoute({
component: LayoutComponent,
});
function LayoutComponent() {
return (
<html lang='en'>
<head>
<meta charSet='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Movie-App</title>
</head>
<body className='bg-black max-w-4xl mx-auto text-white px-5 md:px-0'>
<header className='flex justify-between items-center p-4 bg-[#780909] text-white rounded-b-2xl shadow-xl shadow-[#df0707] mb-6'>
<h1 className='text-2xl flex'>
<Link
to='/'
search={{ page: 1 }}
activeProps={{
className: 'font-bold hello',
}}
activeOptions={{
includeSearch: false,
}}
>
Movies🍿
</Link>
<div className='mx-5'>|</div>
<Link
to='/search'
search={{ q: '' }}
activeProps={{
className: 'font-bold',
}}
>
Search
</Link>
</h1>
<div id='favorites-count'>{/* <FavoritesCount /> */}</div>
</header>
<Outlet /> {/* Start rendering router matches */}
</body>
</html>
);
}
The preceding code does the following:
- Creates a root route component(via
createRootRoute
) that will be displayed on every application page. - Implements basic navigation for our application via the
Link
component for TanStack Router. - Renders other paths that the router will match via the
<Outlet/>
component from TanStack Router. - Finally, applies styles in markup via tailwind classes.
Next, if the npm run dev
command is still running, you should get the following output in your browser.
Browser Output:
Building the Movies Index-Page
In this section, we will build the index page of our app, where all the movies retrieved from an API will be displayed in a paginated view.
N/B: We’ll use the TMDb REST API, which provides information about various popular TV shows and movies. Get an API key.
When you ran the npm run dev
command after setting up TanStack Router, along with the routeTree being generated, placeholder code was also generated for each file in the routes
directory. So, your index.tsx
page should look like this:
// src\routes\index.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: () => <div>Hello/!</div>,
});
In the preceding code, we have a basic route implementation with TanStack Router. Unlike other file-based routers where components are exported, we export the file route instead. We create the file route via createFileRoute function, which accepts a single argument of type string
that represents the path of the file that the route will be inferred from. Finally, we pass in a component
that will be rendered when we hit the route path.
Pagination
Let’s implement the pagination feature to allow us to view data retrieved from the API in paginated segments.
First, we will create the pagination component.
In the src
directory create a subdirectory called components
and then create a paging.tsx
file.
Enter the following code in paging.tsx
import React from 'react';
import { Link } from '@tanstack/react-router';
export default function Paging({
pages,
Route,
page,
}: {
pages: number;
Route: any;
page: number;
}) {
return (
<div className='flex gap-1 text-xl font-bold justify-end'>
{new Array(pages).fill(0).map((_, i) =>
page === i + 1 ? (
<div className='px-4 py-2 border border-red-300 rounded bg-[#0b0000] text-white'>
{i + 1}
</div>
) : (
<Link
key={i}
from={Route.id}
search={{
page: i + 1,
}}
className='px-4 py-2 border border-red-300 rounded hover:bg-[#a33d3da1]'
>
{i + 1}
</Link>
)
)}
</div>
);
}
In the preceding code, we have a basic react presentational component. The pagination logic and conditional rendering based on the logic are in the’ return’ block of the component.
The pagination logic is basically:
- A new array is created with its length set to the number of pages that will be paginated via the
pages
prop. - Each index in the array is filled with
0
—array.fill(0)
- The looped through via the
map()
function, where we check if the current page we have in our search param(viapage
prop) equals the index of the current array item. - Next, a presentational component is shown depending on the result of the preceding condition.
- The Link component is used to navigate to a new page if the conditional is
true
—i.e, the search param(page
) equals the current index(i
). Check the docs for prop options(from, search) used in the Link component. - Finally, markup is styled with tailwind.
Next, we will build the index route, where movies will be retrieved from the API and paginated.
Enter the following code in index.tsx
:
// src\routes\index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";
import { z } from "zod";
import Paging from "../components/Paging";
export const Route = createFileRoute("/")({
component: IndexComponent,
validateSearch: z.object({
page: z.number().catch(1),
}),
});
function IndexComponent() {
const pages = 4;
const { page } = Route.useSearch();
return (
<div>
<div className="flex justify-end pr-5 py-5">
<Paging page={page} pages={pages} Route={Route} />
</div>
</div>
);
}
The preceding code does the following:
- The zod library is used to manage and type our search parameters.
- The
validateSearch
option increateFileRoute
is used to validate our search parameters from the URL via the zod library. In this casepage
must benumber
with a default value of1
. - The
IndexComponent
is a component that will be rendered when the route path is matched. - In
IndexComponent
, useSearch hook is used to access the current value of thepage
search parameter. - The
pages
variable represents the number of pages for pagination. - Required props are passed to the
Paging
component.
Browser output:
Retrieving Data
We will be retrieving the data for our application from the TMDb API which will require an API key, get it here.
Next, create an api.ts
file in the src
directory and enter the following code:
// get all Movies
export async function getMovies(page: number = 1) {
const response = await fetch(
`https://api.themoviedb.org/3/movie/popular?include_adult=false&language=en-US&page=${encodeURIComponent(page)}&api_key=${API_KEY}`
)
.then((r) => r.json())
.then((r) => ({
pages: 4,
movies: r.results,
}));
return response;
}
In the preceding code, the exported getMovies()
function is used to fetch data from the API. It accepts a page
parameter from the URL search parameters. Finally, it returns an object, where the pages
property represents the number of pages, and movies
is the data gotten from the API to be displayed on the UI.
Now, in index.tsx
, enter the following code:
// src\routes\index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";
import Paging from "../components/Paging";
import { getMovies } from "../api";
import { z } from "zod";
export const Route = createFileRoute("/")({
component: IndexComponent,
validateSearch: z.object({
page: z.number().catch(1),
}),
loaderDeps: ({ search: { page } }) => ({ page }),
loader: ({ deps: { page } }) => getMovies(page),
});
function IndexComponent() {
const { page } = Route.useSearch();
const { movies, pages } = Route.useLoaderData();
return (
<div>
<div className="flex justify-end pr-5 py-5">
<Paging page={page} pages={pages} Route={Route} />
</div>
<div>{JSON.stringify(movies, null, 2)}</div>
</div>
);
}
In the preceding code, note the following:
- The
getMovies()
is imported and used. It gives us access to data from the API. - The
/
route support pagination via the search parampage
. For this data to be stored in a cache, it has to be accessed via theloaderDeps
function. - The
loader
gets thepage
search param stored in cache and uses it to get data from the API via thegetMovies()
function. Route.useLoaderData()
gives us access to the data loaded in the loader.- Finally,
JSON.stringify()
is used to show the data on the UI.
Browser Output:
Now, let’s create a presentational component to make our UI more presentable:
In the components
directory create MovieCards.tsx
file, and add the following code:
import { Link } from '@tanstack/react-router';
export default function IndexComponent({ movies }: { movies: any[] }) {
return (
<div className='grid grid-cols-1 md:grid-cols-2'>
{movies.map((m, i) => (
<Link
to='/movies/$movieId'
params={{
movieId: m.id,
}}
className='flex m-2'
key={m.id || i}
>
<img
src={`https://image.tmdb.org/t/p/w500${m.poster_path}`}
className='rounded-tl-lg rounded-bl-lg aspect-w-5 aspect-h-7 w-1/4'
/>
<div className='w-3/4 flex flex-col'>
<div className='font-bold text-xl px-4 bg-[#ba0c0c] text-white py-2 rounded-tr-md'>
{m.original_title}
</div>
<div className='border-red-900 border-b-2 border-r-2 rounded-br-lg flex-grow pt-3'>
<div className='italic line-clamp-2 px-4'>{m.overview}</div>
<div className='flex justify-between px-4 pt-3 items-center'>
{/* <FavoriteButton movieId={m.id} /> */}
<div>{m.vote_average.toFixed(1)} out of 10</div>
</div>
</div>
</div>
</Link>
))}
</div>
);
}
N/B: The path
/movies/$movieId
in theto
property inLink
is not yet defined, so you will see an error. We will fix it in the next section.
Next, we will import this component into index.tsx
, where it will replace JSON.stringify()
, which is currently being used.
.....
import MovieCards from "../components/MovieCards";
....
function IndexComponent() {
const { page } = Route.useSearch();
const { movies, pages } = Route.useLoaderData();
return (
<div>
<div className="flex justify-end pr-5 py-5">
<Paging page={page} pages={pages} Route={Route} />
</div>
<MovieCards movies={movies} />
</div>
);
}
Browser Output:
Our app’s UI looks better now.
Building The Movie Detail Page
The movie detail page displays the details of a single movie when it is clicked on.
Using Path Parameters
According to the Tanstack Official Docs:
Path params are used to match a single segment (the text until the next
/
) and provide its value back to you as a named variable. They are defined by using the$
character prefix in the path, followed by the key variable to assign it to.
To implement the movie details functionality, we will use path parameters, this will allow us to define dynamic routes via each movie id
.
In the routes
directory, create a new movies
subdirectory, in the movies
directory create a $movieId.tsx
. This file will contain the logic for fetching an individual movie from TMDb API.
Enter the following code in $movieId.tsx
:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/movies/$movieId")({
component: MovieDetail,
});
function MovieDetail() {
return <h1>Movie!!!</h1>;
}
The preceding code is a basic implementation of the movie detail functionality without data from an API. The MovieDetail
component is rendered when the path matches /movies/$movieId
, where $movieId
stands for the id
of the movie that is clicked on the index page.
Browser Output:
Now, let’s create a component for the movie details.
In the component
directory, create a Movie.tsx
file, and add the following code:
import type { Movie } from '../types';
export default function Movie({ movie }: { movie: Movie }) {
return (
<div className='flex'>
<div className='flex-shrink w-1/4'>
<img
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
className='aspect-w-5 aspect-h-7 rounded-3xl'
/>
</div>
<div className='w-3/4'>
<div className='font-bold text-2xl px-4'>{movie.title}</div>
<div className='italic text-xl px-4 mb-5'>{movie.tagline}</div>
<div className='pt-3 px-4'>
<div className='italic'>{movie.overview}</div>
<div className='flex justify-between pt-3 items-center'>
<div>{movie.vote_average.toFixed(1)} out of 10</div>
</div>
<div className='grid grid-cols-[30%_70%] pt-3 gap-3'>
<div className='font-bold text-right'>Runtime</div>
<div>{movie.runtime} minutes</div>
<div className='font-bold text-right'>Genres</div>
<div>{movie.genres.map(({ name }) => name).join(', ')}</div>
<div className='font-bold text-right'>Release Date</div>
<div>{movie.release_date}</div>
<div className='font-bold text-right'>Production Companies</div>
<div>
{movie.production_companies.map(({ name }) => name).join(', ')}
</div>
<div className='font-bold text-right'>Languages</div>
<div>
{movie.spoken_languages
.map(({ english_name }) => english_name)
.join(', ')}
</div>
</div>
</div>
</div>
</div>
);
}
Now, let’s get the movie details from the API.
Enter the following code in the api.ts
file.
....
// get Movie by Id
export async function getMovie(id: string) {
const response = await fetch(
`https://api.themoviedb.org/3/movie/${id}?language=en-US&api_key=${API_KEY}`
).then((r) => r.json());
return response;
}
The preceding code is a fetch request that retrieves details for individual movies based on their id
.
Now, enter the following code in $movieId.tsx
:
import { createFileRoute } from "@tanstack/react-router";
import { getMovie } from "../../api";
import Movie from "../../components/Movie";
export const Route = createFileRoute("/movies/$movieId")({
component: MovieDetail,
loader: ({ params: { movieId } }) => getMovie(movieId),
});
function MovieDetail() {
const movie = Route.useLoaderData();
return <Movie movie={movie} />;
}
The above code does the following:
- The
getMovie()
function is imported fromapi.tsx
. - No need for
loaderDeps
, because we are working with path params. We simply pass the parameter intoloader
. useLoaderData()
is used to access data from theloader
.- The
<Movie/>
component is used to display the movie detail on the UI.
Browser Output:
Building Movie Search
The search feature will enable us to search for specific movies in our app. So, we will build a search route to implement this feature.
Search Params for State Management
We will be storing our search query in the URL via search params, the code below shows how this is done with Tanstack Router.
In the routes
directory, create a search.tsx
file. Add the following code:
import { createFileRoute, useNavigate} from "@tanstack/react-router";
import { useState } from "react";
interface SearchParams {
query: string;
}
export const Route = createFileRoute("/search")({
component: SearchRoute,
validateSearch: (search: { query: string }): SearchParams => {
return {
query: (search.query as string) || "",
};
},
});
function SearchRoute() {
const { query } = Route.useSearch();
const navigate = useNavigate({ from: Route.id });
const [newQuery, setNewQuery] = useState(query);
return (
<div className="p-2">
<div className="flex gap-2">
<input
value={newQuery}
onChange={(e) => {
setNewQuery(e.target.value);
}}
onKeyUp={(e) => {
if (e.key === "Enter") {
navigate({
search: (old: { query: string }) => ({
...old,
query: newQuery,
}),
});
}
}}
className="border-2 border-gray-300 rounded-md p-1 text-black w-full"
/>
<button
onClick={() => {
navigate({
search: (old: { query: string }) => ({
...old,
q: newQuery,
}),
});
}}
>
Search
</button>
</div>
// Results
</div>
);
}
The preceding code does the following:
- We define the
/search
route with thecreateFileRoute()
function from TanStack Router. - The
validateSearch
option is used to validate the search params of the/search
route, it also returns a typedSearchParams
object with a query property set tostring
. - The
SearchRoute
component accesses the search param viaRoute.useSearch()
. - We use the
useNavigate
function from TanStack Router to programmatically navigate from the/search
route —useNavigate({ from: Route.id })
.[] - The
useState
hook for React is used to update the state of theSearchRoute
component based on the search paramquery
. - In the
return
block, we update the search param based on theinput
value
typed in by the user as a search query. Also, theuseNavigate
function is used to update search string(query
) to what is currently being typed by the user via the SearchParamOptions type
Browser Output:
In the above demo, you will notice that the search param in the URL got updated to the query that was entered in the search box.
Showing Search Results
With the search functionality in place, the next step is to display the search result(s). For this, we will create a nested route inside the search route, which will also serve as an index route of the search route.
Nested Routing
The Outlet Componet is used to create nested routes in TanStack Router. Since the search route will be the parent route of the nested route, the outlet component is used here:
// src\routes\search.tsx
import { createFileRoute, useNavigate, Outlet } from "@tanstack/react-router";
....
function SearchRoute() {
....
return (
<div className="p-2">
<div className="flex gap-2">
.....
</div>
<Outlet />
</div>
);
}
In the preceding code, the <Outlet/>
component was added to the SearchRoute
component in search.tsx
. This is where the result(s) for our search query will be displayed.
First, let’s retrieve the search results data from the API. Add the following code in api.ts
......
// Search Movie
export async function searchMovie(query: string = "") {
const response = await fetch(
`https://api.themoviedb.org/3/search/movie?query=${encodeURIComponent(
query
)}&include_adult=false&language=en-US&page=1&api_key=${API_KEY}`
)
.then((r) => r.json())
.then((r) => r.results);
return response;
}
In the preceding code is the logic for retrieving search results from the TMDb API.
Next, in the routes
directory, create the search.index.tsx
file, and add the following code:
import { createFileRoute } from "@tanstack/react-router";
import MovieCards from "../components/MovieCards";
import { searchMovie } from "../api";
interface SearchParams {
query: string;
}
export const Route = createFileRoute("/search/")({
component: SearchRoute,
loaderDeps: ({ search: { query } }) => ({ query }),
loader: async ({ deps: { query } }) => {
const searched_movies = await searchMovie(query);
return {
searched_movies,
};
},
validateSearch: (search: { query: string }): SearchParams => {
return {
query: (search.query as string) || "",
};
},
});
function SearchRoute() {
const { searched_movies } = Route.useLoaderData();
return (
<>
<MovieCards movies={searched_movies || []} />
</>
);
}
In the preceding code:
- We defined the
/search/
file route, which is the index route of the search route/search
validateSearch
is used to get the query, which will enable the interior index(/search/
) to be routed into the<Outlet/>
of the parent route(/search
).- The
searchMovie()
function retrieves movie results from the API via thequery
parameter. loaderDeps
caches the search query, which is then used by theloader
to fetch the search results.- Finally,
useLoaderData
allows the search results to be accessed by theSearchRoute
component.SearchRoute
renders the results on the UI via the<MovieCards/>
component.
Browser Output:
Streaming Search Results
In this section, we are going to add some lazy-loading and code-splitting to our application.
We will display details for the first movie returned from the search results at the top of the page, the defer function and Await component from TanStack Router will be used to enable other search results to be displayed sooner, without waiting for the details of the first movie to be rendered.
Sample UI:
Fallbacks with React Suspense
Enter the following code in search.index.tsx
:
import { createFileRoute, defer, Await } from "@tanstack/react-router";
import { Suspense } from "react";
import MovieCards from "../components/MovieCards";
import Movie from "../components/Movie";
import { searchMovie, getMovie } from "../api";
interface SearchParams {
query: string;
}
export const Route = createFileRoute("/search/")({
component: SearchRoute,
loaderDeps: ({ search: { query } }) => ({ query }),
loader: async ({ deps: { query } }) => {
const searched_movies = await searchMovie(query);
return {
searched_movies,
firstMovie: searched_movies?.[0]?.id
? defer(getMovie(searched_movies[0].id))
: null,
};
},
validateSearch: (search: { query: string }): SearchParams => {
return {
query: (search.query as string) || "",
};
},
});
function SearchRoute() {
const { searched_movies, firstMovie } = Route.useLoaderData();
// fallbacks with React Suspense
return (
<>
{firstMovie && (
<div className="my-5">
<Suspense fallback={<div>Loading...</div>}>
<Await promise={firstMovie}>
{(movie) => {
return <Movie movie={movie} />;
}}
</Await>
</Suspense>
</div>
)}
<MovieCards movies={searched_movies || []} />
</>
);
}
In the preceding code:
getMovie()
calls the same API endpoint as movie details page in$movieId.tsx
- In
loader
, the data(first movie details) fromgetMovie()
is deferred if the data is available via the defer function. defer()
allows us to lazy-load the data(first movie details), in case the API endpoint for the movie details takes time, we can show other search results before the first movie details.- In the
return
block of theSearchRoute
component: Suspense
is used to provide a fallback until the promise(first movie details) from theAwait
component is resolved.- The first movie details is rendered via
<Movie/>
component to the UI.
Browser Output:
Summary
In this article, you built a single-page movie application that lets you view movies, search for movies, and get the details of individual movies with React and TanStack Router. You learned how to use some basic and advanced features of TanStack Router like typesafe routes and links, nested layouts, advanced data loader capabilities, search params as a React State replacement, and integration with React Suspense.