TanStack Router: Query Parameters & Validators

balastrong

Leonardo Montini

Posted on March 7, 2024

TanStack Router: Query Parameters & Validators

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>
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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>);
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Navigate with the params

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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

validateSearch: (search) => v.parse(ItemFilters, search),
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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


Thanks for reading this article, I hope you found it interesting!

I recently launched a GitHub Community! We create Open Source projects with the goal of learning Web Development together!

Join us: https://github.com/DevLeonardoCommunity

Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️
You can find it here:
YouTube

Feel free to follow me to get notified when new articles are out ;)

💖 💪 🙅 🚩
balastrong
Leonardo Montini

Posted on March 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related