Recently, I thought of trying out Tanstack Router coz I wanted to explore the file based routing. And to my surprise, it is really good. So much so that I replaced React Router completely.
The beautiful part is it has so many good features. And one of the feature I was really impressed was how it handles navigation blocking and that also with custom component. I was looking for this feature for so long and seeing it here so well implemented.
A little bit of frusttration might happen initially but it is all worth it. It makes route restrictions so easy to implement and along with that the role based restrictions as well. I totally loved it.
lets start
As in my previous post we have seen that I was using createBrowserRouter([]) of React Router inside a component called AppRoutes which is used in the App.tsx file. I just changed all of the contents inside AppRoutes only with the same structure in App.tsx with just a little change.
Tanstack Router uses something called tanstackRouter plugin which maintains a routeTree.gen.ts that gets updated whenever we create a file route inside routes folder. So, it generates a template which is used to handle the route and its corresponding component.
So, it has few rules:
- routes folder has __root.tsx (__ its double underscore) which acts as a root layout of the app
- index.tsx file which acts as the main component which will render as a child to the __root.tsx and its our home page
- every folder inside routes folder is a route and must have one file called index.tsx. As soon as we create this file, the plugin generates the code automatically about the component(like we used to do rafce or rfce using react vscode extension) and maintain its data in routeTree.gen.ts
- any folder, we dont want to be treated as route, we can prefix the name of the folder with “-“(hyphen) and all of its file are ignored to be acted as route
- if we need pathless routes we can prefix the folder with “_”(single underscore). Pathless means, we can have a folder like auth/login which will create a link/route as http://localhost/auth/login, but if we want the route to be /login only in the browser we can just rename auth to _auth which will make auth folder ignored by Tanstack Router.
All other things you can read in their docs which is pretty solid but this would get things started. I will keep things only which was new to me and lot of practical features which I liked.
middleware
In the react router, we had an array that takes the middlewares as functions which guards the route. But, as we discussed it has some issues as it gets passed and we added some loaders and all other stuff to somehow make it work for asynchronous calls. But, in tanstack router it is really simple and straightforward. It has something called beforeLoad() in its route level for every component or layout file which actually works pretty well in this scenario.
export async function checkAuth({ location }: { location: { href: string } }) { const accessToken = useTokensStore.getState().accessToken if (!accessToken) { return redirect({ to: '/auth/login', search: { redirect: location.href }, }) } const userId = useAuthUserStore.getState().id if (userId) { return } try { const err = await useAuthUserStore.getState().fetchUserProfile() // might throw on network error if (err) { throw redirect({ to: '/auth/login', search: { redirect: location.href }, }) } } catch (error) { // Re-throw redirects (they're intentional, not errors) if (isRedirect(error)) throw error // Auth check failed (network error, etc.) - redirect to login throw redirect({ to: '/auth/login', search: { redirect: location.href }, }) }}
This is the auth middleware which we can attach in the beforeLoad(). And its so simple and straight. If you observe that it has redirect: location.href. What actually it does that if session expires and user is redirected to /auth/login page and if user logins again, it will automatically redirected to the page where the user was previously on.
export const Route = createFileRoute('/_sidebarLayout')({ beforeLoad: checkAuth, pendingComponent: AuthLoader, component: SidebarLayout,})
As, you can see this is the sidebarlayout folder’s route.tsx which is a pathless route. Just by adding the checkAuth() that we made earlier makes it restricted unless authenticated. And another beautiful thing is the pendingComponent, where we can actually show some loader while we are fetching session data from the backend so that user does not see a blank page for better user experience. AuthLoader is just a component.
const AuthLoader = () => { return ( <div className="fixed inset-0 z-50 flex min-h-svh w-full items-center justify-center bg-muted p-6 md:p-10"> <div className="w-full max-w-sm text-center"> <Badge variant="outline"> <Spinner data-icon="inline-start" /> Authenticating... </Badge> </div> </div> )}export default AuthLoader
This is what I liked. Simple, synchronous and what actually needed.
role restriction
export const Route = createFileRoute('/_sidebarLayout/_admin')({ beforeLoad: checkRoles(['admin']), component: AdminLayout,})
Inside sidebarlayout, I made an _admin folder which also acts as a pathless route layout and added a role restriction, so that all the routes under this folder is accessible to admin only. Simple.
export function checkRoles(allowedRoles: Roles[]) { return () => { const role = useAuthUserStore.getState().role if (!allowedRoles.includes(role)) { return redirect({ to: '/dashboard', replace: true }) } }}
If not admin, we will just redirect to dashboard which is available for every kind of user.
404 or not found route
Tanstack provides 404 routes for each layout but, I wanted a general route where each of the unmatched route will be forwarded to, no matter where the user is. We can do it in the RouteProvider present in the AppRoutes.tsx
// Create a new router instanceconst router = createRouter({ routeTree, defaultNotFoundComponent: NotFound })// Register the router instance for type safetydeclare module '@tanstack/react-router' { interface Register { router: typeof router }}export default function AppRoutes() { return <RouterProvider router={router} />}
I have a NotFound component file which is just a UI component which I attached here. Any unmatched route will be redirected to this component
navigation blocker
So, I have a form to create a product in the route /products/manage, which is also the form to update the product. I mean the same route but different actions which is pretty common.
The only thing react router had which is not in tanstack is route state. I used to pass the row data from the products table to product form via route state and was really worked well. And the most surprising was, even if I reload tha page it was there. Thats what reatc router v7 did. I know that it should not be done like that but I did and probably I will change this to get the data from the backend using the producy id which is also standard. So, for this I did kept the data in a zustand store before navigating to the /manage route and get it the re using the store.
But, we need to clear the state as well when we move out from the product form after updating the data otherwise the product will always have the data filled, coz zustand store does not get empty by default. So, what I did that I called the resetStore() in the cleanup function in the useEffect(). Pretty straight forward right. But, the issuse which came here is the strict mode of react that cleared the store twice, making the form empty even if in the update action. Frustrating
Tanstack router to the rescue!!! In the route template, there is a hook like beforeLoad() called onLeave() which gets called only when the route is left. In thay hook, I called the resetStore()
export const Route = createFileRoute('/_sidebarLayout/_admin/products/manage')({ component: CreateProductPage, // This only fires when the user leaves the /manage URL path! onLeave: () => { useProdUpdateData.getState().resetStore() },})
The CreateProductPage is also the same page that contains the product form to create and reused for update as well.
I was talking about blocking navigation, right. I had made a hook called useProductFormmState() which supplies state to the product form UI to bind data/state to the form. In that hook we can do somehting liek this:
// 1. Hook directly into TanStack Form's state tracker
const isFormDirty = useSelector(productForm.store, (state) => state.isDirty)
const isSubmitted = useSelector(productForm.store, (state) => state.isSubmitted)
useBlocker({
shouldBlockFn: () => {
if (!isFormDirty) return false
if (isSubmitted) return false
const shouldLeave = confirm('Are you sure you want to leave?')
return !shouldLeave
},
// Keeps browser reloads/tab closures synced with the exact same states
enableBeforeUnload: () => isFormDirty && !isSubmitted,
})
useBLocker() is provided by tanstack router whcih checks if the form submitted and if any of the form field content is updated/changed. And it stops the navigation if form is dirty and not submitted. If you see there is 2 more lines above useBlocker() that checks for wether the form is submitted or dirty. The only thing which I think the Tanstack is making me do it is using its store. You cant use any other library to track the form but its own tanstack store. Before, its used to be present in the form itsef, but for the new versions, we have to use its store to track the form. Although, it has form.subscribe, but its internal to form. We still can apply some hack and get it outside to track it but I thought lets just use its store.
That’s it. Thanks
Leave a comment