Passing data between routes in Tanstack Router with router state or search params
14 May 2025
Long story short, I’ve been working on a feature that requires a set of data shared between few routes in sequential order.
As much as I like using state management tools, but seriously tanstack router is taking us to a whole different level. Where simplicity and safety is key!
I’ve created this small example of where users gotta choose there favourite music genre on the main page, and then we’ll navigate to a page that displays the selected genre and probably provide some recommendations based on it. I’ll be showing how to do it using router state and search params assuming you have a running React app with Tanstack Router configured with two routes: /
and /suggestions
.
Using Router State
What we’re doing here is something similar to the browser’s native History: pushState method.
First off, we’ll need to register the types for HistoryState
in our module declaration in src/main.tsx
file (i’m using default here, feel free to place it where ever your app configured at).
// src/main.tsx
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
// we added this interface to the module declaration
interface HistoryState {
curator?: {
name: Genre;
};
}
}
Genre
is a union type of string literals defined in my src/routes/index.tsx
file which we’re going to explore now.
// src/routes/index.tsx
import { useNavigate } from "@tanstack/react-router";
export type Genre = "oriental" | "jazz" | "techno" | "house" | "qawwali";
export type GenreItem = {
name: Genre;
};
const MUSIC_GENRES: GenreItem[] = [
{ name: "oriental" },
{ name: "jazz" },
{ name: "techno" },
{ name: "house" },
{ name: "qawwali" },
] as const;
const Genre = () => {
const navigate = useNavigate();
return (
<div>
<h1>Select your favorite genre</h1>
<div className="box-wrapper">
{MUSIC_GENRES.map((genre) => (
<div
className="box"
key={genre.name}
onClick={() =>
navigate({
to: "/suggestions",
state: { curator: { name: genre.name } },
})
}
>
{genre.name}
</div>
))}
</div>
</div>
);
};
Let me explain quickly what i’ve created here:
- I’ve created
Genre
, string literal union type to prevent any typos or invalid genres. GenreItem
is a type for objects that havename
property of typeGenre
.MUSIC_GENRES
is a constant array ofGenreItem
objects that is readonly to prevent any accidental modification of the array allowing TS to infer the exact type of the array.Genre
component is a simple component that renders a list of genres and navigates to the/suggestions
route when a genre is clicked, allowing us to optionally pass key ofcurator
intostate
field during navigation.
Now, let’s create the /suggestions
route and start to consume this data.
// src/routes/suggestions.tsx
import { useRouterState } from "@tanstack/react-router";
const Suggestions = () => {
const state = useRouterState({ select: (s) => s.location.state });
return <p>{state.curator?.name}</p>;
};
What we did? just used useRouterState
hook from Tanstack Router to get the curator
state from the state
field of the location
object.
Using Search Params
Well, another way is to just pass data using url search params like: /suggestions?genre=oriental
.
Again, assuming we’ve a clean react app. let’s start sending search params from index.tsx
to suggestions.tsx
route.
This time, we’re gonna be using zod
and change a bit of the schema, we only need a string. let’s strip that curator object out.
// src/routes/index.tsx
// ... same imports
export const GENRES = [
"oriental",
"jazz",
"techno",
"house",
"qawwali",
] as const;
export type Genre = (typeof GENRES)[number];
const Genre = () => {
const navigate = useNavigate();
return (
<div>
<h1>Select your favorite genre</h1>
<div className="box-wrapper">
{GENRES.map((genre) => (
<div
className="box"
key={genre}
onClick={() =>
navigate({
to: "/suggestions",
search: {
genre: genre,
},
})
}
>
{genre}
</div>
))}
</div>
</div>
);
};
export const Route = createFileRoute("/")({
component: () => <Genre />,
});
What happens? just a readonly tuple with each string becoming a literal type, then export a union type of that tuple.
This way, user’s are navigating to /suggestions?genre=jazz
and we’re able to consume it in suggestions.tsx
route.
// src/routes/suggestions.tsx
import { z } from "zod";
import type { Genre } from "./index";
import { GENRES } from "./index";
const suggestionsSearchSchema = z.object({
genre: z.enum(Object.values(GENRES) as [Genre, ...Genre[]]),
});
// type SuggestionsSearch = z.infer<typeof suggestionsSearchSchema>;
const Suggestions = () => {
const { genre } = Route.useSearch();
return <p>{genre}</p>;
};
export const Route = createFileRoute("/suggestions")({
component: Suggestions,
validateSearch: (search: Record<string, unknown>) =>
suggestionsSearchSchema.parse(search),
});
Let’s break this down quickly:
- we’ve created a
suggestionsSearchSchema
usingzod
(and Typescript assertion where its value must be one of the strings found inGENERES
) to validate the search params. - we’ve used
Route.useSearch
hook to get the search params. - we’ve used
validateSearch
option to validate the search params.
That’s it! leverage that to redirect back to the home page or custom 404 if the search params are invalid.