TanStack Router + Query with PropelAuth

If you've spent any time in the React ecosystem recently, you've likely noticed a massive shift in how developers handle routing and data fetching. The days of wrestling with complex useEffect chains are fading. In their place, TanStack Query and TanStack Router have become the go-to tools.
But what exactly are they?
- TanStack Router: A fully type-safe, client-side routing library with first-class support for search params, nested layouts, and route-level data loading. TypeScript catches routing errors at compile time, giving you fine-grained control over navigation and URL state.
- TanStack Query: A data-fetching and server-state management library that handles caching, background refetching, and async data synchronization - without you having to write that logic yourself.
When combined, TanStack Router's route-level loaders and TanStack Query's caching give you routes that prefetch and cache data intelligently. The result: fewer redundant network requests, a snappier experience for users, and a cleaner, fully type-safe codebase.
Introducing authentication into this flow does require some care, though. You need to ensure your user's auth state and access tokens are ready and injected into your router before data fetches fire.
This guide will walk you through how to integrate PropelAuth into these powerful tools. By the end of this guide, you'll have a fully authenticated, data-fetching route that looks like this:
export const Route = createFileRoute('/dashboard')({
loader: ({ context }) => {
const { tokens, userClass } = context.auth
return context.queryClient.ensureQueryData({
queryKey: ['dashboard-data'],
queryFn: async () => {
const accessToken = await tokens.getAccessToken()
return fetch('/api/dashboard', {
headers: { Authorization: `Bearer ${accessToken}` }
}).then(r => r.json())
}
})
},
component: Dashboard,
})
function Dashboard() {
const data = Route.useLoaderData()
const { userClass } = Route.useRouteContext().auth
return <div>Welcome, {userClass.email}! Your data: {JSON.stringify(data)}</div>
}
Prerequisites
We'll be using a React Vite project with TanStack Router and TanStack Query already installed. See the docs below if you need to set them up:
Installing PropelAuth’s React Library
Install the @propelauth/react library:
npm i @propelauth/react
Next, wrap your application in either the AuthProvider. The AuthProvider is responsible for checking if the current user is logged in and fetching information about them. You should add it to the top level of your application so it never unmounts.
You can find your Auth URL in the Frontend Integration page of the PropelAuth Dashboard.
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { AuthProvider } from '@propelauth/react'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider authUrl="ENTER_YOUR_AUTH_URL_HERE">
<App />
</AuthProvider>
</React.StrictMode>
)
With PropelAuth successfully wrapping our application, the next step is to pass the user’s authentication state directly into TanStack Router's context.
Injecting Auth State into TanStack Router Context
With PropelAuth installed, we need to handle a common race condition: the router might try to load a protected page before PropelAuth finishes checking the user's auth status.
To fix this, we'll build an InnerApp component that pauses the router's initialization until auth is fully loaded. Once auth.loading is false and we confirm that the user is logged in, we safely inject the auth info and our TanStack Query client directly into the router's context.
// src/App.tsx
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAuthInfo, RedirectToLogin } from '@propelauth/react'
import { routeTree } from './routeTree.gen'
const queryClient = new QueryClient()
// Initialiaze the router and define the context.
const router = createRouter({
routeTree,
context: {
auth: undefined!,
queryClient,
},
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function InnerApp() {
const auth = useAuthInfo()
// 1. Wait for PropelAuth to initialize and confirm user is authenticated
if (auth.loading) {
return <div>Loading...</div>
}
if (!auth.isLoggedIn) {
return <RedirectToLogin />
}
// 2. Inject the loaded auth state into the router context
return <RouterProvider router={router} context={{ auth, queryClient }} />
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<InnerApp />
</QueryClientProvider>
)
}
Now that auth state is flowing into the router, we need to tell TanStack Router what that data looks like. We'll define a custom context at the root of our routing tree.
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import type { UseAuthInfoLoggedInProps } from '@propelauth/react'
interface MyRouterContext {
auth: UseAuthInfoLoggedInProps;
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: RootComponent,
})
function RootComponent() {
return (
<div>
<Outlet />
</div>
)
}
Both our auth state and query client are now available to all downstream routes. We can pull the userClass and tokens classes from the context and use them to make a post request to our backend, like so:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/welcome')({
component: Welcome,
})
function Welcome() {
const { auth } = Route.useRouteContext()
const { userClass, tokens } = auth
const handlePost = async () => {
const accessToken = await tokens.getAccessToken();
const res = await fetch('/api/example', {
method**: '**POST**',**
headers: {
Authorization: `Bearer ${accessToken}`
}
})
return res.json()
}
return (
<div>
<div>Hello, {userClass.email}</div>
<button onClick={handlePost}>Post Data</button>
</div>
)
}
With auth state and the query client available throughout the router, we can now fetch protected data cleanly and efficiently - without a single useEffect in sight.
Secure Data Fetching with React Query
Instead of fetching data inside components, we can make authenticated requests during the routing phase itself. By accessing context.auth and context.queryClient inside a route loader, we can preload the data before the component tree even mounts.
queryClient.ensureQueryData returns cached data immediately if it exists, or runs the fetch if it doesn't. tokens.getAccessToken() ensures we always have a fresh token (it’ll reuse a cached one if it’s recent enough for perf reasons) that our backend can verify to protect our API routes.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
const { tokens } = context.auth;
return context.queryClient.ensureQueryData({
queryKey: ['dashboard-data'],
queryFn: async () => {
const accessToken = await tokens.getAccessToken();
const res = await fetch('/api/dashboard', {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
return res.json()
}
})
},
component: About,
})
function Dashboard() {
// Data is guaranteed to be available synchronously at render time
const data = Route.useLoaderData()
return (
<div>
<div>{JSON.stringify(data)}</div>
</div>
)
}
Wrapping Up
By combining PropelAuth, TanStack Router, and TanStack Query, you get a robust authentication and data-fetching setup that's both type-safe and lightning fast. Auth state is loaded before the router initializes, data is cached and prefetched at the route level, and protected API calls are handled cleanly.
From here, you can extend this pattern to handle role based access control, route-level redirects for unauthenticated users, and more.