Доки по разработке
This project is maintained by teniryte
TanStack Router хранит состояние маршрутизации во внутреннем store и позволяет подписываться на изменения через useRouterState/router.subscribe. Это удобно для глобального UI (progress bar, breadcrumbs, title) и аналитики.
const status = useRouterState({ select: (s) => s.status })
location — { pathname, search, hash, state }. Часто удобно мемоизировать только pathname, чтобы не перерендеривать layout при изменении query.resolvedLocation — фактический URL после всех нормализаций (удаляйте лишние / именно тут).matches — массив активных маршрутов с данными loaders, meta и context.status — обычно idle | pending. Можно использовать как флажок для глобальных спиннеров.isTransitioning / isLoading — булевы derived-поля, пригодны для disable-кнопок.pendingLocation — информация о следующем переходе (например, для оптимистичных UI).fetchCount и pendingMatches — показывают, сколько loaders сейчас в полёте.Контекст задается при создании:
const router = createRouter({
routeTree,
context: {
auth,
queryClient,
},
})
context доступен в loader, beforeLoad и в компонентах через Route.useRouteContext():
const { auth } = Route.useRouteContext()
interface RouterContext {
auth: ReturnType<typeof createAuthClient>
queryClient: QueryClient
featureFlags: FeatureFlagService
}
export const router = createRouter({
routeTree,
context: (() => {
const auth = createAuthClient()
const queryClient = new QueryClient()
return { auth, queryClient, featureFlags: createFlags(auth) }
})(),
})
В тестах обычно создают отдельный router с тестовым context (фейковые auth/api clients).
useRouterState({ select, structuralSharing, equalityFn }) мемоизирует результат и предотвращает перерендер. Возвращайте “плоские” примитивы или мемоизированные объекты.const { pathname, status } = useRouterState({
select: ({ location, status }) => ({ pathname: location.pathname, status }),
equalityFn: (a, b) => a.pathname === b.pathname && a.status === b.status,
})
router.subscribe((state) => ...) — низкоуровневая подписка (очищайте вручную). Удобно для логгеров или адаптеров внешнего стора.Лайфхак: если нужно реактивно управлять заголовком страницы, можно сделать кастомный хук:
export function useDocumentTitle() {
const meta = useRouterState({
select: (state) => state.matches.at(-1)?.context?.title ?? state.location.pathname,
})
useEffect(() => {
document.title = meta
}, [meta])
}
Для метрик обычно достаточно комбинации router.subscribe + router.history.subscribe/listen (в зависимости от history-адаптера).
Главный принцип: подписки должны жить в одном “инфраструктурном” модуле и всегда отписываться при размонтировании.
| Метод | Назначение |
| — | — |
| router.invalidate() | Глобальный refetch всех loaders. |
| router.load() | Префетч дерева маршрутов (используется в SSR). |
| router.dehydrate() / router.hydrate() | Работа с SSR state. |
| router.navigate({ to, search, replace, resetScroll }) | Гибкая навигация с точным контролем поведения скролла. |
| router.buildLocation({ to, params, search }) | Получить URL-объект без перехода (для ссылок/OG-тегов). |
const href = router.buildLocation({
to: '/projects/$projectId',
params: { projectId },
search: (old) => ({ ...old, filter }),
})
context.Route.useRouteContext() вместо глобальных синглтонов.beforeLoad можно синхронно читать глобальное состояние (redux store).const useThemeStore = create((set) => ({ theme: 'light', toggle: () => set(...) }))
const unsub = router.subscribe((state) => {
if (state.location.pathname.startsWith('/settings')) {
useThemeStore.getState().toggle()
}
})
// позже: unsub()
router.subscribe((state) => {
reduxStore.dispatch(routerStateUpdated(state))
})
Не тащите UI-стор внутрь context: вместо этого пробрасывайте сервисы/клиенты, а сами UI-состояния держите в своих сторах.
RouterDevtools, вкладка Router State.useRouterState({ select: (state) => state.location }) внутри custom hooks для логирования изменений.window.router = router в dev-режиме позволяет вручную вызывать router.navigate из консоли.router.subscribe в dev и логируйте только нужные поля.location.state.modal = 'editUser' и рендерьте модалку вне маршрутов, подписавшись на useRouterState.navigate сохраняйте target в analytics и показывайте skeleton нового маршрута.router.history и прокидывайте пути в мобильную оболочку.router.preloadRoute({ to }), чтобы данные уже лежали в кэше.useRouterState/useRouteContext, а не глобальные импорты.dehydrate/hydrate.equalityFn или кастомные мемоизации.