Доки по разработке
This project is maintained by teniryte
Loaders исполняются при первом совпадении маршрута и автоматически переисполняются при изменении params, search, context или пользовательских loaderDeps. Они кешируются на уровне маршрута, поэтому повторные переходы используют уже загруженные данные, пока не истек gcTime.
export const Route = createRoute({
getParentRoute: () => rootRoute,
path: 'invoices/$invoiceId',
loaderDeps: ({ params, search }) => ({
invoiceId: params.invoiceId,
currency: search.currency ?? 'usd',
}),
loader: async ({ loaderDeps, context, signal }) => {
const res = await context.api.getInvoice(loaderDeps.invoiceId, {
currency: loaderDeps.currency,
signal,
})
return res.data
},
shouldReload: ({ context }) => context.flags.forceReloadInvoices,
gcTime: 5 * 60 * 1000,
component: InvoicePage,
})
loaderDeps позволяет вручную описать зависимости и тем самым контролировать кеш-ключ. Удобно, когда нужно учитывать кусок context (например, tenantId) или вручную минимизировать кол-во перезагрузок.shouldReload можно использовать для адаптивного refetch (например, когда пользователь переключает офлайн/онлайн или меняется feature-flag).gcTime + staleTime позволяют балансировать между «кешировать всё» и «всегда свежо». staleTime = Infinity превращает loader в SSR/SSG источник данных.preload: true в конфиге маршрута подсказывает Routerу прогревать loader заранее при hover/visible prefetch.const invoice = Route.useLoaderData({
// доступно начиная с v1.38
select: (data) => ({
total: data.total,
customerName: data.customer.name,
}),
structuralSharing: true,
})
const status = useRouterState({ select: (s) => s.status })
if (status === 'pending') return <Skeleton />
await router.preloadRoute({
to: '/invoices/$invoiceId',
params: { invoiceId },
search: { currency: 'eur' },
})
router.preloadRoute позволяет реализовать hover/viewport prefetch.router.invalidate() или инвалидации кэша Query.router.dehydrate()/router.hydrate(...).beforeLoad и guardsbeforeLoad работает синхронно/асинхронно до loader. Здесь удобно:
context (например, подгрузить feature-flags);redirect, notFound или кастомные ошибки.beforeLoad: async ({ context, params, location }) => {
await context.auth.ensureSession()
if (!context.auth.hasRole('billing')) {
throw redirect({
to: '/403',
search: { from: location.href, requiredRole: 'billing' },
})
}
}
Лайфхак: возвращайте из beforeLoad объект, который попадёт в loaderContext текущего и дочерних маршрутов, например { featureFlags }, чтобы не гонять один и тот же fetch в каждом слое.
team → project → invoice) и объединяйте данные через Route.useRouteContext() вместо одного «толстого» запроса.context.loaderClient.get({ key }).AbortController и прерывайте дочерние fetch’и при отмене родительского signal.Actions живут рядом с маршрутом и автоматически получают доступ к context, params, submission, signal и parsedBody.
export const Route = createFileRoute('/settings/profile')({
component: ProfileForm,
action: async ({ context, submission, signal }) => {
const payload = submission.value
const result = await context.api.updateProfile(payload, { signal })
if (!result.ok) {
return {
status: 'error',
fieldErrors: result.validation,
message: 'Не удалось обновить профиль',
}
}
return redirect({
to: '/settings/profile',
search: { flash: 'updated' },
})
},
})
const action = Route.useAction({
onEachSuccess: (result, { invalidate, loaderClient }) => {
invalidate() // только текущий маршрут
loaderClient.invalidateLoader({ key: ['user'] })
},
})
const handleSubmit = (formData: FormData) =>
action.submit(formData, {
headers: { 'x-csrf-token': window.csrf },
onSuccess: ({ result }) => toast.success(result.message ?? 'Saved'),
onError: (err) => toast.error(err.message ?? 'Ошибка'),
})
Лайфхаки:
<form> используйте <Form route={Route}>, тогда отправка автоматически вызывает action.submit.Route.useActionState() даёт { status, submission, error, isPending } — удобно для disable кнопок или optimistic UI.json({ ... }, { status: 422 }) и ловить это в error boundary, либо обработать в компоненте через action.state.error.router.invalidate() — полная перезагрузка дерева, используйте редко.invalidateQueries) и затем локальный ререндер страницы.defer позволяет вернуть микс синхронных и отложенных частей. Это работает и на сервере, и на клиенте.
loader: async ({ context }) =>
defer({
hero: context.api.getHero(), // ждём
stats: context.api.getStats(), // промис пойдёт в поток
suggestions: context.api.getSuggestions(), // можно завернуть в timeout
})
const data = Route.useLoaderData()
return (
<>
<Hero {...data.hero} />
<Suspense fallback={<StatsSkeleton />}>
<Await promise={data.stats}>{(stats) => <Stats data={stats} />}</Await>
</Suspense>
<Suspense fallback={null}>
<Await promise={data.suggestions} catch={(err) => <ErrorBox err={err} />}>
{(items) => <Suggestions items={items} />}
</Await>
</Suspense>
</>
)
Советы:
withTimeout(promise, 2000) и обрабатывайте ошибку в Await/errorComponent.Стандартный паттерн — использовать loader для гарантии данных, а в компоненте useQuery только для чтения/рефетча.
export const Route = createFileRoute('/invoices/$invoiceId')({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData({
queryKey: ['invoice', params.invoiceId],
queryFn: ({ signal }) => context.api.getInvoice(params.invoiceId, { signal }),
staleTime: 60_000,
}),
component: InvoicePage,
})
const { params } = Route.useRouteContext()
const invoiceQuery = useQuery({
queryKey: ['invoice', params.invoiceId],
enabled: false, // данные уже прогреты
})
await queryClient.invalidateQueries({ queryKey: ['invoice', id] }) внутри onSuccess, а затем при необходимости router.invalidate().router.dehydrate() включает данные обеих систем.throw router.notFound() для 404 вместо new Error, чтобы триггерить notFoundComponent.throw redirect({ ... }) прямо из loader/action — Router остановит обработку и поменяет маршрут.loader: try/catch и возвращайте { status: 'error', message } — тогда UI решает, что показать.maxAge/staleWhileLoading в loaderClientOptions для повторных попыток без дёргания UI.router.options.defaultPendingComponent помогает увидеть все точки ожидания.router.state.matches содержит актуальные loaderData — удобно логировать в DevTools.debug: true для createRouter, чтобы видеть в консоли граф переисполнений.signal и уважают отмену.Route.useLoaderData/useLoader.beforeLoad, а не внутри компонентов.action или Query приводят к invalidate/setLoaderData, чтобы данные не протухали.<Suspense> + <Await>.router.loadRoute, preload: true) настроен на ключевые маршруты.retry в API/Query и user-friendly fallback.