Dev Highlights

Доки по разработке

This project is maintained by teniryte

Router Store

TanStack Router хранит состояние маршрутизации в собственном сторе (тонкая обертка над Zustand). Его можно читать и подписываться на изменения для построения UI вне компонента маршрута, для тулбаров, заголовков страниц или интеграции с аналитикой.

const status = useRouterState({ select: (s) => s.status })

Основные поля состояния

Дополнительно доступен router.state.matchesById, что полезно для быстрых lookup-ов без перебора массива.

const router = useRouter()
const currentUserId = router.state?.matchesById['/users/$userId']?.params.userId

Контекст router

Контекст задается при создании:

const router = createRouter({
  routeTree,
  context: {
    auth,
    queryClient,
  },
})

context доступен в loader, beforeLoad, action, component через Route.useContext():

const { auth } = Route.useContext()

Типизация контекста

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) }
  })(),
}) satisfies RootRouteOptions<RouterContext>

Используйте satisfies для безопасной проверки структуры. В тестах можно подменять контекст через router.update({ context: { ...router.options.context, auth: mockAuth }, }).

Подписки и селекторы

const { pathname, status } = useRouterState({
  select: ({ location, status }) => ({ pathname: location.pathname, status }),
  equalityFn: (a, b) => a.pathname === b.pathname && a.status === b.status,
})

Лайфхак: если нужно реактивно управлять заголовком страницы, можно сделать кастомный хук:

export function useDocumentTitle() {
  const meta = useRouterState({
    select: (state) => state.matches.at(-1)?.context?.title ?? state.location.pathname,
  })
  useEffect(() => {
    document.title = meta
  }, [meta])
}

Глобальные события

router.events.on({
  onBeforeLoad: (event) => log(event),
  onLoad: (event) => analytics.track('route_loaded', event),
  onResolved: () => console.log('done'),
})

События помогают для трекинга, метрик и спиннеров на уровне приложения.

Практические сценарии:

Не забудьте отписаться:

const unsub = router.events.subscribe('onBeforeLoad', handler)
return () => unsub()

Router API полезные методы

| Метод | Назначение | | — | — | | router.invalidate() | Глобальный refetch всех loaders. | | router.invalidateRoute(opts) | Точечная инвалидация. | | router.load() | Префетч дерева маршрутов (используется в SSR). | | router.dehydrate() / router.hydrate() | Работа с SSR state. | | router.reset() | Очистка стора (редко нужно). | | router.update(opts) | Горячая подмена опций (контекст, routeTree) без размонтирования. | | router.navigate({ to, search, replace, resetScroll }) | Гибкая навигация с точным контролем поведения скролла. | | router.buildNext({ to, params }) | Просто получить URL без перехода (для ссылок/OG-тегов). |

const href = router.buildNext({
  to: '/projects/$projectId',
  params: { projectId },
  search: (old) => ({ ...old, filter }),
})

Интеграция с внешними стейт-менеджерами

Пример с Zustand

const useThemeStore = create((set) => ({ theme: 'light', toggle: () => set(...) }))

router.events.on({
  onResolved: ({ toLocation }) => {
    if (toLocation.pathname.startsWith('/settings')) {
      useThemeStore.getState().toggle()
    }
  },
})

Пример для Redux DevTools

router.subscribe((state) => {
  reduxStore.dispatch(routerStateUpdated(state))
})

Не тащите UI-стор внутрь context: вместо этого пробрасывайте сервисы/клиенты, а сами UI-состояния держите в своих сторах.

Debug

if (import.meta.env.DEV) {
  router.store.subscribe((state) => {
    console.debug('[router]', state.location.pathname, state.status)
  })
}

Примеры из практики

Чек-лист