This guide covers debugging common TanStack Router problems, from route matching failures to navigation issues and performance problems.
Use TanStack Router DevTools for real-time debugging, add strategic console logging, and follow systematic troubleshooting patterns to identify and resolve router issues quickly.
Install and configure the DevTools for the best debugging experience:
npm install @tanstack/router-devtools
// src/App.tsx
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
function App() {
return (
<div>
<RouterProvider router={router} />
{/* Only shows in development */}
<TanStackRouterDevtools router={router} />
</div>
)
}
DevTools Features:
Enable debug mode for detailed console logging:
const router = createRouter({
routeTree,
defaultPreload: 'intent',
context: {
// your context
},
// Enable debug mode
debug: true,
})
Add router to global scope for debugging:
// In development only
if (import.meta.env.DEV) {
window.router = router
}
// Console debugging commands:
// router.state - current router state
// router.navigate() - programmatic navigation
// router.history - navigation history
Symptoms:
Debugging Steps:
// ❌ Common mistake - missing leading slash
const route = createRoute({
path: 'about', // Should be '/about'
// ...
})
// ✅ Correct
const route = createRoute({
path: '/about',
// ...
})
// Debug route tree in console
console.log('Route tree:', router.routeTree)
console.log('All routes:', router.routesById)
// Ensure parent route is properly defined
const childRoute = createRoute({
getParentRoute: () => parentRoute, // Must return correct parent
path: '/child',
// ...
})
Symptoms:
Debugging Steps:
// ❌ Wrong parameter syntax
path: '/users/{id}' // Should use $
// ✅ Correct parameter syntax
path: '/users/$userId'
const route = createRoute({
path: '/users/$userId',
// Add parameter validation/parsing
params: {
parse: (params) => ({
userId: Number(params.userId), // Convert to number
}),
stringify: (params) => ({
userId: String(params.userId), // Convert back to string
}),
},
component: () => {
const { userId } = Route.useParams()
console.log('User ID:', userId, typeof userId) // Debug output
return <div>User {userId}</div>
},
})
function DebugParams() {
const location = useLocation()
const params = Route.useParams()
console.log('Current pathname:', location.pathname)
console.log('Parsed params:', params)
return null // Just for debugging
}
Symptoms:
Debugging Steps:
// ❌ Common mistakes
<Link to="about">About</Link> // Missing leading slash
<Link href="/about">About</Link> // Wrong prop (href instead of to)
// ✅ Correct
<Link to="/about">About</Link>
function NavigationDebug() {
const navigate = useNavigate()
const handleNavigate = () => {
console.log('Attempting navigation...')
navigate({
to: '/dashboard',
search: { tab: 'settings' },
})
.then(() => console.log('Navigation successful'))
.catch((err) => console.error('Navigation failed:', err))
}
return <button onClick={handleNavigate}>Navigate</button>
}
// Ensure component is inside RouterProvider
function ComponentWithNavigation() {
const router = useRouter() // Will throw error if outside provider
console.log('Router state:', router.state)
return <div>...</div>
}
Symptoms:
Debugging Steps:
const route = createRoute({
path: '/dashboard',
beforeLoad: ({ context, location }) => {
console.log('Before load - location:', location.pathname)
console.log('Auth state:', context.auth)
if (!context.auth.isAuthenticated) {
console.log('Redirecting to login...')
throw redirect({ to: '/login' })
}
},
// ...
})
// Add to router configuration
const router = createRouter({
routeTree,
context: {
/* ... */
},
// Log all navigation events
onNavigate: ({ location, type }) => {
console.log(`Navigation (${type}):`, location.pathname)
},
})
Symptoms:
Debugging Steps:
const route = createRoute({
path: '/posts',
loader: async ({ params, context }) => {
console.log('Loader called with params:', params)
try {
const data = await fetchPosts()
console.log('Loader data:', data)
return data
} catch (error) {
console.error('Loader error:', error)
throw error
}
},
component: () => {
const data = Route.useLoaderData()
console.log('Component data:', data)
return <div>{/* render data */}</div>
},
})
function DataLoadingDebug() {
const location = useLocation()
console.log('Route status:', {
isLoading: location.isLoading,
isTransitioning: location.isTransitioning,
})
return null
}
const route = createRoute({
path: '/posts/$postId',
loader: async ({ params }) => {
// Loader will re-run when params change
console.log('Loading post:', params.postId)
return fetchPost(params.postId)
},
// Add dependencies for explicit re-loading
loaderDeps: ({ search }) => ({
refresh: search.refresh,
}),
})
Symptoms:
Debugging Steps:
const route = createRoute({
path: '/search',
validateSearch: (search) => {
console.log('Raw search params:', search)
const validated = {
q: (search.q as string) || '',
page: Number(search.page) || 1,
}
console.log('Validated search params:', validated)
return validated
},
component: () => {
const search = Route.useSearch()
console.log('Component search:', search)
return <div>Query: {search.q}</div>
},
})
function SearchDebug() {
const navigate = useNavigate()
const currentSearch = Route.useSearch()
const updateSearch = (newSearch: any) => {
console.log('Current search:', currentSearch)
console.log('New search:', newSearch)
navigate({
to: '.',
search: (prev) => {
const updated = { ...prev, ...newSearch }
console.log('Final search:', updated)
return updated
},
})
}
return (
<button onClick={() => updateSearch({ q: 'test' })}>Update Search</button>
)
}
Symptoms:
Debugging Steps:
// Wrap your app for profiling
import { Profiler } from 'react'
function App() {
return (
<Profiler
id="Router"
onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} took ${actualDuration}ms`)
}}
>
<RouterProvider router={router} />
</Profiler>
)
}
// ❌ Subscribes to all search params
function MyComponent() {
const search = Route.useSearch()
return <div>{search.someSpecificField}</div>
}
// ✅ Subscribe only to specific field
function MyComponent() {
const someSpecificField = Route.useSearch({
select: (search) => search.someSpecificField,
})
return <div>{someSpecificField}</div>
}
// Add to router configuration
const router = createRouter({
routeTree,
context: {
/* ... */
},
onUpdate: (router) => {
console.log('Router state updated:', {
pathname: router.state.location.pathname,
isLoading: router.state.isLoading,
matches: router.state.matches.length,
})
},
})
Symptoms:
Debugging Steps:
function MyComponent() {
const [data, setData] = useState(null)
useEffect(() => {
const subscription = someService.subscribe(setData)
// ✅ Always clean up subscriptions
return () => {
subscription.unsubscribe()
}
}, [])
return <div>{data}</div>
}
function DebuggableComponent() {
useEffect(() => {
console.log('Component mounted')
return () => {
console.log('Component unmounted')
}
}, [])
return <div>Content</div>
}
Symptoms:
Debugging Steps:
// Ensure this declaration exists
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
# Check if route types are being generated
ls src/routeTree.gen.ts
# Regenerate route types if needed
npx @tanstack/router-cli generate
function TypeDebugComponent() {
const params = Route.useParams()
const search = Route.useSearch()
// Add type assertions to check what TypeScript infers
console.log('Params type:', params as any)
console.log('Search type:', search as any)
return null
}
When debugging any router issue, start by collecting this information:
function RouterDebugInfo() {
const router = useRouter()
const location = useLocation()
useEffect(() => {
console.group('🐛 Router Debug Info')
console.log('Current pathname:', location.pathname)
console.log('Search params:', location.search)
console.log('Router state:', router.state)
console.log('Active matches:', router.state.matches)
console.log('Route tree:', router.routeTree)
console.groupEnd()
}, [location.pathname])
return null
}
// Add to your app during debugging
;<RouterDebugInfo />
Create minimal reproduction:
// Minimal route for testing
const testRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/debug',
component: () => {
console.log('Test route rendered')
return <div>Debug Route</div>
},
})
// Add to route tree temporarily
const routeTree = rootRoute.addChildren([
// ... other routes
testRoute, // Add test route
])
// In browser console (when router is on window)
// Current router state
router.state
// Navigate programmatically
router.navigate({ to: '/some-path' })
// Get route by path
router.getRoute('/users/$userId')
// Check if route exists
router.buildLocation({ to: '/some-path' })
// View all registered routes
Object.keys(router.routesById)
Monitor these requests when debugging:
const router = createRouter({
routeTree,
context: {
/* ... */
},
onUpdate: (router) => {
performance.mark('router-update')
},
onLoad: (router) => {
performance.mark('router-load')
performance.measure('router-load-time', 'router-update', 'router-load')
},
})
const route = createRoute({
path: '/slow-route',
loader: async () => {
const start = performance.now()
const data = await fetchData()
const end = performance.now()
console.log(`Loader took ${end - start}ms`)
return data
},
})
After debugging router issues, you might want to: