TanStack Router exposes router lifecycle events through router.subscribe. This is useful for imperative side effects like analytics, resetting external state, or running DOM-dependent logic after navigation.
router.subscribe takes an event name and a listener, then returns an unsubscribe function:
const unsubscribe = router.subscribe('onResolved', (event) => {
console.info('Navigation finished:', event.toLocation.href)
})
// Later, clean up the listener
unsubscribe()
const unsubscribe = router.subscribe('onResolved', (event) => {
console.info('Navigation finished:', event.toLocation.href)
})
// Later, clean up the listener
unsubscribe()
router.subscribe is best for imperative integrations that need to observe navigation without driving rendering:
If you need reactive UI updates, prefer framework hooks like useRouterState, useSearch, and useParams instead of subscribing manually.
TanStack Router emits these lifecycle events:
For the full event payload types, see the RouterEvents type.
For a normal navigation, the events usually flow like this:
You usually do not need every event. A good rule of thumb is:
Navigation events receive location change metadata describing what changed:
const unsubscribe = router.subscribe('onBeforeNavigate', (event) => {
console.info({
from: event.fromLocation?.href,
to: event.toLocation.href,
pathChanged: event.pathChanged,
hrefChanged: event.hrefChanged,
hashChanged: event.hashChanged,
})
})
const unsubscribe = router.subscribe('onBeforeNavigate', (event) => {
console.info({
from: event.fromLocation?.href,
to: event.toLocation.href,
pathChanged: event.pathChanged,
hrefChanged: event.hrefChanged,
hashChanged: event.hashChanged,
})
})
A few useful details:
onResolved is a good default for analytics because it fires after navigation finishes:
const unsubscribe = router.subscribe('onResolved', ({ toLocation }) => {
analytics.track('page_view', {
path: toLocation.pathname,
href: toLocation.href,
})
})
const unsubscribe = router.subscribe('onResolved', ({ toLocation }) => {
analytics.track('page_view', {
path: toLocation.pathname,
href: toLocation.href,
})
})
If you use a mutation library without keyed mutation state, clear it after navigation:
const unsubscribe = router.subscribe('onResolved', ({ pathChanged }) => {
if (pathChanged) {
mutationCache.clear()
}
})
const unsubscribe = router.subscribe('onResolved', ({ pathChanged }) => {
if (pathChanged) {
mutationCache.clear()
}
})
Use onRendered when your side effect depends on the new route content already being in the DOM:
const unsubscribe = router.subscribe('onRendered', ({ toLocation }) => {
focusPageHeading(toLocation.pathname)
})
const unsubscribe = router.subscribe('onRendered', ({ toLocation }) => {
focusPageHeading(toLocation.pathname)
})
If you subscribe from a component or framework effect, always return the unsubscribe function from your cleanup so the listener is removed when the component unmounts.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.