Let's try Tanstack Router.

Published on: Jan 13, 2024 | 8 min read |

In this blog post, we'll try implementing a simple app with the Tanstack router.

Tanner Linsley the creator of Tanstack Query (previous react-query) and other such popular react libraries just released the v1 of the Tanstack router and it's been getting some hype ever since. So one might ask what is this new router ? and why do we even need a router in the age of meta frameworks which comes with a routing solution already. As per the official docs Tanstack router is "A fully type-safe React router with built-in data fetching, stale-while revalidate caching and first-class search-param APIs." Tanstack router could primarily be used for single page applications (p.s. they are not going anywhere) but we can use it as for SSR as well (more on than in later posts). Let's get started.

Installation.

Before we begin let's install it, you can any package manager of your choice, I'll go with yarn.

yarn add @tanstack/react-router

Getting Started

Routes are the most important part of Tanstack router and they can be created with RootRoute and Route classes. We can nest multiple routes inside each other to create a route tree.

Root Route

Every route tree should begin with an root route and can be created as

import { RootRoute } from '@tanstack/react-router';

const rootRoute = new RootRoute({
  component: RootComponent,
});

RootRoute class optionally accepts a object in which we can define the component we want to render for the route. Now that we have created our root route we would need to add an outlet.

The Oulet component is used to render child routes and can be rendered anywhere in our component tree. We'll render it in our root component as below.

import { Outlet } from '@tanstack/router';

function RootComponent() {
  return (
    <div>
       <div className="p-2 flex gap-2">
        <Link to="/" className="[&.active]:font-bold">
          Home
        </Link>{" "}
        <Link to="/about" className="[&.active]:font-bold">
          About
        </Link>
      </div>
      <hr />
      <Outlet />
    </div>
  );
}

As you may see, we have a navbar in our root component with following links / and about, But if you were to click on them nothing will render (we will come to 404 routes later in the post) and we are using the built in Link component from Tanstack router.

Creating Routes

We have setup our root route and an outlet, now it's time to create some child routes. We will create a index route / and /about routes. Route can be created with the Route class.

import { Route } from '@tanstack/router';

const indexRoute = new Route({
  path: '/',
  component: function Home() {
    return (
      <div className="p-2">
        <h3>Tanstack router example</h3>
      </div>
    );
  },
  getParentRoute: () => rootRoute,
});

const aboutRoute = new Route({
  path: '/about',
  component: function About() {
    return <div className="p-2">Hello from About!</div>;
  },
  getParentRoute: () => rootRoute,
});

We pass a configuration object to the Route specifying the path and the component to render when this path is matched and then we specify the parent route for this route and we do that with the getParenRoute.

Creating a route tree and setting up our router.

Now that we have created our routes it's time to create our route tree. To create a route tree, we need to add all the children routes to our root route as below.

 const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);

Now we need to create a router with our route tree, which can be created using the Router class from tanstack router

import { Router } from '@tanstack/router';

const router = new Router({
  routeTree
});

We created our router instance using the Router class and passed our routeTree to it. Now we need to pass our router instance to the RouteProvider to complete our routing setup.

import { RouteProvider } from '@tanstack/router';

<RouteProvider
  router={router}
/>

Now if you click on the links in the navbar, it will render our route correctly.

Dynamic Routes

So far we have only looked at how to create static routes, but you may wonder what about dynamic routes. Dynamic routes are also created with Route class and their path starts with $ symbol followed by the label. We can acess dyanmic segments using the useParams hook throughout our app.

const blogRoute = new Route({
  path: '/blog',
  component: Blog
 getParentRoute: () => rootRoute,
});

const blogPostRoute = new Route({
  path: '$blogId'
  component: BlogPost,
  getParentRoute: () => blogPostRoute,
});

const routeTree = rootRoute.addChildren([blogRoute.addChildren([blogPostRoute])]);

For eg: For route /blog/1, will produce a param of blogId with value 1.

Tanstack router provides a Link component for navigating between routes. All we have to do is pass the path of the route to the Link component's to prop.

import { Link } from '@tanstack/router';

<Link to="/about">About Me</Link>

We can also use the same component for navigating to dynamic routes like below.

<Link
  to="/users/$userId"
  params={{
    userId: 1, 
  }}
>
 Prateek
</Link>

We can also pass query params to our link component like below

<Link
  search={{
    filter: 'prateek' ,
  }}
>
 Results
</Link>

Link component provides support two more props as activeProps and inactiveProps for styling the active and inactive states of our link. These props accepts object with css styles or css classes. Here's how we can use these props to style our link.

// styling with css styles object.
<Link
 to="/"
 activeProps={{
   style: {
      background: 'blue',
      color: '#fff',
    }
 }}
>
 Home
</Link>


// styling with css classes.
<Link
 to="/"
 activeProps={{
   classes: 'text-white bg-blue-600'
 }}
>
 Home
</Link>

useNavigate

Tanstack router also provides a useNavigate to navigate to ours routes imperatively. The useNavigate hooks returns a navigate function which is used for navigation.

const navigate = useNavigate();

async function login() {
  try {
    await loginMutation({
      email,
      password,
    }
    
    navigate({to: "/home"});

  } catch (error) {
    console.error(error);  
  }
}

we can also pass an optional params object with dynamic path segments to the navigate function.

Another way we could navigate to a route is with the Navigate component, it allows to us redirect when a component mounts or when a certain condition is met. One example of this is when we want to redirect the to a login page if the user is not logged in.

Rest of the component code ...

if (notAuthenticated) {
  return <Navigate to="/login" />;
}

Rest of the component code ...

Going Deeper

Next we'll move to data fetching, Tanstack router provides loaders inspired from Remix's loaders, they allow us to fetch data for a route. For this we'll create couple of routes as /users and /users/$id and will fetch some dummy users using the json placeholder api.

// api services...
export async function fetchUsers() {
  cons res = await fetch('https://jsonplaceholder.typicode.com/users');
  if (res.ok) {
    return await res.json();
  }

  return [];
}

export async function fetchUserById(userId: number) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  if (res.ok) {
    return await res.json();
  }

  return null;
}

We'll create our routes like this. We'll first create the /users route.

const usersRoute = new Route({
  getParentRoute: () => rootRoute,
  path: '/users',
  loaders: () => fetchUsers(),
  component: Users,
});

export function Users() {
  const users = usersRoute.useLoaderData();
  return (
    <div>
       {users.length === 0 && <span>No users found</span>}
       {users.length > 0 && (
         <ul className="space-y-3">
           {users.map((user) => (
             <li>
                <Link to="/users/$userId" params={{userId: user.id}}>
                  <span>{user.name}</span>
                </Link>
             </li>
           ))}
         </ul>
       )}
    </div>
  );
}

One thing we did different from our previous route(s) declaration is that, we defined a loader option and passed in the result of our fetchUsers function. Now we can access our data in our component with useLoaderData utility available on our route instance.

Now we'll create our user detail route and pass a function which returns our user data to the loader option, since we need an id to fetch a user, we can read it from the userId (because we have defined our route as $userId) property available on the params object passed to our loader function. We can also read the param value from the useParams method available on the route instance.

const userRoute = new Route({
  getParentRoute: () => rootRoute,
  path: '/users/$userId',
  loaders: ({ params }) => fetchUsersById(+params.userId),
  component: Users,
});

export function Users() {
  const user = userRoute.useLoaderData();
  const { userId } = userRoute.useParams();
  console.log('userId - ', userId);

  return (
    <div>
       {user ? (
         <div>
           Name  - {user.name}
           UserName  - {user.username}
           Email  - {user.email}
           Website  - {user.website}
         </div>
       ) : 'User not found'}
    </div>
  )
}

Creating our route tree and setting up our router.

const routeTree = rootRoute.addChildren([
  usersRoute,
  userRoute,
]);

const router = new Router({
  routeTree,
})

Now if we navigate to the /users route, Users component is rendered and if we scroll further down, Tanstack router will preload all unique $userId route path along with the data defined in the loader (preloading is enabled by default in Tanstack router), so that if we navigate to any user's page, our page will load instantly.

Pathless Routes

Pathless routes are routes that don't have a path but an id instead to uniquely identify them. Pathless routes are useful in following cases

  • Using a shared layout for all child routes
  • Common data dependency
  • Sharing context with child routes.
  • Provide global error and pending component for the route subtree.

NotFoundRoute

Routes which don't match any path segments or dynamic path segments are caught by splat routes or NotFoundRoute(s). We can place a NotFoundRoute at every root/parent routes but if we have many subtrees it could become tedious, instead we can create a global NotFoundRoute and pass it to our routeTree instead. Here'an example of a NotFoundRoute:-

const globalNotFoundRoute = new NotFoundRoute({
  getParentRoute: () => rootRoute,
  component: () => {
    return <span>Page not found</span>
  },
});

const router = new Route({
  routeTree,
  notFoundRoute: globalNotFoundRoute,
});

Conclusion

We have at looked some of the basics and important features of Tanstack router but there still so much more to explore. Tanstack router is a feature packed library with some very notable features like file-based routing, SSR support, code-splitting, full type safety and much more and it has the potential to be the de-facto router for single page applications not just for react.

I am planning to write another blog posts covering some advanced use-cases like search params based querying, filtering and pagination.

You can find the source code for the blog below. github

Related Tags
reacttanstack router
Loading comments...