Доки по разработке
This project is maintained by teniryte
router и вызовите await router.load(), передав URL из запроса.router.dehydrate() и передайте результат в HTML.router.hydrate(dehydratedState).<RouterProvider router={router} />.routeTree, но создавайте новый router и context на каждый HTTP-запрос.initialState не только location, но и matches, если хотите сократить повторную работу router.load на клиенте.router.options.context, формируйте серверный контекст (cookies, user, feature flags) синхронно до router.load, чтобы матчи могли сразу читать его в beforeLoad.hydrateRoot в startTransition, чтобы Suspense-переходы были плавнее.// server.ts
const router = createRouter({ routeTree, context })
await router.load({
initialState: {
location: createMemoryHistoryLocation(request.url),
},
})
const dehydrated = router.dehydrate()
const html = renderToString(
<RouterProvider router={router} />
)
// client.tsx
const router = createRouter({ routeTree, context })
router.hydrate(window.__ROUTER_STATE__)
hydrateRoot(document, <RouterProvider router={router} />)
Частый паттерн — прогонять loaders и React Query вместе, чтобы не дублировать fetch-логику.
// server.ts
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: { queryClient },
})
await router.load({
preload: true,
initialState: {
location: createMemoryHistoryLocation(request.url),
},
})
const dehydratedRouter = router.dehydrate()
const dehydratedQuery = dehydrate(queryClient)
return renderToString(
<RouterProvider router={router} context={{ dehydratedQuery }} />
)
На клиенте: сначала hydrate(dehydratedRouter), затем queryClient.hydrate(dehydratedQuery) и только потом hydrateRoot.
router.load({ preload: true }) на сервере, чтобы подготовить дочерние маршруты до первого байта.router.dehydrate(), а остальной UI догружайте через router.preloadRoute после hydrateRoot.lazyRouteComponent + route.options.staleTime, чтобы не перезапрашивать данные при последующей клиентской навигации.router.preloadRoute — ручное предзагружение.preload="viewport" — загрузка при попадании ссылки в viewport.beforeLoad + router.load() позволяют подготавливать данные перед переходом (например, в background task).Link принимает preload, preloadDelay, preloadMaxAge, preloadGcMaxAge — комбинируйте их, чтобы не перегружать сеть.router.preloadRoute можно вызывать из useEffect, подписываясь на события скролла и предугадывая переходы.lazy(() => import('./route')) в createRoute. Префетч автоматически вытянет и код, и данные.router.buildLocation({ to: '/dashboard' }) и передавайте его в router.preloadRoute(...), чтобы прогревать варианты query-параметров.signal в fetch-клиент.Если вы используете TanStack Query: вызывайте queryClient.prefetchQuery внутри loader, затем queryClient.dehydrate.
const postsRoute = createFileRoute('/posts')({
beforeLoad: async ({ context }) => {
await context.queryClient.prefetchQuery({
queryKey: ['posts', 'list'],
queryFn: () => fetchPosts(),
staleTime: 60_000,
})
},
})
Так как beforeLoad выполняется и на сервере, и на клиенте, этот код автоматически работает и для SSR, и для клиентского префетча.
defaultPreload, defaultPreloadDelay, defaultPreloadStaleTime и defaultPreloadMaxAge на createRouter(...) задают глобальную стратегию прогрева.router.subscribe(...) и замеряйте, сколько времени занял preload; результаты можно сохранять в метриках и динамически регулировать агрессивность префетча.postMessage, чтобы воркер скачал API и статику параллельно.router.options.defaultPreloadStaleTime можно увеличить, чтобы данные считались актуальными дольше.navigator.onLine и router.invalidate() при восстановлении соединения.loader сначала читайте кэш (IndexedDB/Cache Storage). Если данные найдены и свежие — возвращайте их как fallback.router.invalidate() для маршрута.window.addEventListener('online', ...) и инициируйте router.invalidate() или router.load(), чтобы догрузить свежие данные.errorComponent, чтобы сразу показывать офлайн-состояние./router-data/* и храните ответы в Cache Storage.router.dehydrate() в IndexedDB после каждого успешного load, чтобы клиент мог стартовать с последнего известного состояния без сети.clients.matchAll и шлите сообщение router:refresh, которое на клиенте вызывает router.invalidate().Если маршрут триггерит мутацию, регистрируйте navigator.serviceWorker.ready.then(sw => sw.sync.register('sync-post')), а в onSubmit возвращайте optimistic UI. Когда sync завершается, вызывайте router.invalidate() или queryClient.invalidateQueries.
defer из loaders и используйте renderToPipeableStream (React 18).const invoiceRoute = createRoute({
getParentRoute: () => invoicesRoute,
path: '$invoiceId',
loader: async () => ({
invoicePromise: fetchInvoice(),
audit: await fetchAuditSync(),
}),
component: () => (
<Suspense fallback={<Skeleton />}>
<InvoiceDetails />
</Suspense>
),
})
router.dehydrate(), чтобы клиент продолжил ожидать те же pending-данные.renderToPipeableStream используйте onShellReady для отправки HTML сразу после готовности критического маршрута и onAllReady для закрытия стрима.redirect и отвечайте статусом 302/307.router.notFound() можно интегрировать с ответом 404.throw redirect({ to: '/login', statusCode: 302 }) работает как на сервере, так и на клиенте, поэтому используйте единую функцию requireAuth.router.load в try/catch, чтобы корректно обработать redirect/notFound и выставить HTTP-статус.throw notFound({ resetScroll: false }), чтобы не сбрасывать позицию, если пользователь уже был на странице.router.options.defaultNotFoundComponent, чтобы не дублировать 404-разметку во всех ветках дерева.createMemoryHistory), который TanStack Router создает автоматически при router.load({ initialState }).createBrowserHistory.routeTree и сопоставимый context.createHashHistory) на клиенте не забудьте передать history: createHashHistory() в createRouter, иначе гидратация сломается.createMemoryHistory({ initialEntries: [request.url] }), если нужно симулировать push/pop до рендера.router.load() перед рендером.router.dehydrate() и queryClient.dehydrate() синхронно сохраняются и восстанавливаются.