Dev Highlights

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

This project is maintained by teniryte

Router Store

TanStack Router хранит состояние маршрутизации во внутреннем store и позволяет подписываться на изменения через useRouterState/router.subscribe. Это удобно для глобального UI (progress bar, breadcrumbs, title) и аналитики.

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

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

Контекст router

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

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).

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

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.subscribe + router.history.subscribe/listen (в зависимости от history-адаптера). Главный принцип: подписки должны жить в одном “инфраструктурном” модуле и всегда отписываться при размонтировании.

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

| Метод | Назначение | | — | — | | 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 }),
})

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

Пример с Zustand

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

const unsub = router.subscribe((state) => {
  if (state.location.pathname.startsWith('/settings')) {
    useThemeStore.getState().toggle()
  }
})

// позже: unsub()

Пример для Redux DevTools

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

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

Debug

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

Чек-лист