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 have name property of type Genre.
  • MUSIC_GENRES is a constant array of GenreItem 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 of curator into state 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 using zod (and Typescript assertion where its value must be one of the strings found in GENERES) 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.