· TanStack Router · 5 min read

TanStack Router: Query Parameters & Validators

A state manager in the URL, typesafe

A state manager in the URL, typesafe

Welcome to the third article of a series where we will explore TanStack Router, the new typesafe routing library (and state manager, in some cases) for React.

In the first article we saw how to set up a new project with TanStack Router and how to create a couple of routes.

In the second article instead, the focus was on path parameters and loaders.

In this article, we’ll see how you can properly parse and handle query parameters (aka search parameters) and how they can be effectively used as a state manager. Besides, imagine being able to share your state by just passing the URL, how convenient!

Define & Validate your Query Parameters

Here’s a demo of what you’re going to learn in this article. For written instructions, keep reading below.

Imagine having a search page or a table with a lot of inputs for filtering the data, or maybe a dashboard with many configurable widgets. You can build your own customized view by setting those filters but as soon as you refresh the page, they’re gone.

Ok, you can save them on localStorage or somewhere in the browser but… what if you want to send your exact same configuration to someone else? A friend? A colleague? Having your settings directly as query parameters in the URL is probably the easiest way, but handling them manually is far from being an easy task.

TanStack Router is designed with this scenario in mind, giving you great control over query params (aka search params). Let’s see how!

Note: I’m assuming you already have the library set up, if not you can check my setup video.

Create your route

Let’s begin with a /search.tsx empty page and add the link to it in your __root.tsx.

<Link to="/search" activeProps={activeProps}>
  Search
</Link>

Define the type

Now if we want typescript to help us enforcing and validating the correct types, we need to create them.

type ItemFilters = {
  query: string;
  hasDiscount: boolean;
  categories: Category[];
};

type Category = 'electronics' | 'clothing' | 'books' | 'toys';

Write the validating function

With that defined, we should tell TanStack Router that our /search route has those params. We can do that by implementing and typing the validateSearch function.

Your search.tsx file at this point should look like this:

export const Route = createFileRoute("/search")({
  component: Search
  validateSearch: (search: Record<string, unknown>): ItemFilters => {
    return {
      query: search.query as string,
      hasDiscount: search.hasDiscount === 'true',
      categories: search.categories as Category[],
    };
  },
});

function Search() {
    return (<div>Hello /search!</div>);
}

Read the params

Ok cool, now you want to read them I suppose. If you followed the previous chapters, you probably already know that you can use the Route object in your component and get some hooks from there. useSearchParams is the answer.

Update your Search component to look like this:

function Search() {
  const { query, hasDiscount, categories } = Route.useSearch();

  return <pre>{JSON.stringify({ query, hasDiscount, categories }, null, 2)}</pre>;
}

Now if you noticed, there should be an error on ___root.tsx exactly at the Link we created a few steps ago. This is a good sign! TanStack Router is informing us that our path has some required parameters you must provide. Let’s do it.

<Link
  to="/search"
  activeProps={activeProps}
  search={{
    query: 'hello',
    hasDiscount: true,
    categories: ['electronics', 'clothing'],
  }}
>
  Search
</Link>

The error is gone! It’s time to go on your browser and click the link. You can now see a page with your params rendered as JSON.

## Validation with a validating library

That kind of validator function we defined works, but we can do something better by using an existing validating library such as zod or valibot. Let’s see the latter in an example.

npm i valibot

Define your schema

You can now replace your type with the valibot schema.

import * as v from 'valibot';

const Category = v.union([v.literal('electronics'), v.literal('clothing'), v.literal('books'), v.literal('toys')]);

const ItemFilters = v.object({
  query: v.optional(v.string()),
  hasDiscount: v.optional(v.boolean()),
  categories: v.optional(v.array(Category)),
});

type ItemFilters = v.Output<typeof ItemFilters>;
type Category = v.Output<typeof Category>;

With that, your validation function will be as easy as this:

validateSearch: (search) => v.parse(ItemFilters, search),

Query params as application state

The last cool feature we’re gonna see today is how you can in fact use the query params as your application state. Until now we are only able to read them, but you can indeed write them in your app and see them updated real time in the URL.

The trick is to use the navigate function to set our params to the new state, in sync with our input.

In the video I build this step by step and the final result is as follows:

function Search() {
  const { query, hasDiscount, categories } = Route.useSearch();
  const navigate = useNavigate({ from: Route.fullPath });

  const updateFilters = (name: keyof ItemFilters, value: unknown) => {
    navigate({ search: (prev) => ({ ...prev, [name]: value }) });
  };

  return (
    <div>
      <h2>Search</h2>
      You searched for: <input
        value={query}
        onChange={(e) => {
          updateFilters('query', e.target.value);
        }}
      />
      <input type="checkbox" checked={hasDiscount} onChange={(e) => updateFilters('hasDiscount', e.target.checked)} />
      <select
        multiple
        onChange={(e) => {
          updateFilters(
            'categories',
            Array.from(e.target.selectedOptions, (option) => option.value)
          );
        }}
        value={categories}
      >
        {['electronics', 'clothing', 'books', 'toys'].map((category) => (
          <option key={category} value={category}>
            {category}
          </option>
        ))}
      </select>
      <pre>{JSON.stringify({ query, hasDiscount, categories }, null, 2)}</pre>
    </div>
  );
}

We can now edit all query params, see the changes in the URL and easily share the state with anyone else by just… sending them the link!

Conclusion

We now have a state manager perfectly in sync with our URL, easy to share and validated with Typescript to ensure our params are always correct, with no unnecessary or missing values.

Wow, this library is so powerful! I hope you’re enjoying this series on TanStack!

The next chapter will be about authenticated routes, basically some pages that can only be accessed if a condition is met, for example the user is logged in. Stay tuned


Watch the full playlist on YouTube: TanStack Router


You can find the full code on this repository on the 03-query-parameters. Leave a ⭐️ if you found the demo code useful!


About the author
Leonardo

Hello! My name is Leonardo and as you might have noticed, I like to talk about Web Development and Open Source!

I use GitHub every day and my favourite editor is Visual Studio Code... this might influence a little bit my conent! :D

If you like what I do, you should have a look at my YouTube Channel!

TanStack Router (5 Parts Series)
You might also like
Back to Blog