Data Loading
Furin loaders run on the server before rendering. Route loaders live on createRoute(), and page loaders live on route.page().
Route Loader
Use createRoute() when the data should feed a layout or be shared with child pages.
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"
export const route = createRoute({
parent: rootRoute,
loader: async ({ request }) => {
const user = await getSession(request)
return { user }
},
layout: ({ children, user }) => (
<div>
<DashboardNav user={user} />
{children}
</div>
),
})
Page Loader
Use route.page() when the data is specific to one page.
import { route } from "./_route"
export default route.page({
loader: async ({ user }) => {
// user comes from the parent route loader — await it to access its value
const { id } = await user
const stats = await getDashboardStats(id)
return { stats }
},
component: ({ stats, user }) => <Dashboard user={user} stats={stats} />,
})
Loader Context
Loaders receive a context object with:
| Property | Type | Description |
|---|---|---|
request | Request | the incoming Request — direct value |
params | TParams | validated params from createRoute({ params }) — direct value |
query | TQuery | validated query values from createRoute({ query }) — direct value |
set, headers, cookie, redirect | — | server helpers — direct values |
| parent data fields | Promise<T> | each field returned by an ancestor loader is an individually-awaitable Promise<T> |
Why parent fields are Promises
All loaders in the chain start immediately in parallel. A child loader receives parent data as a Promise<T> so it can:
- Opt in to waiting —
const u = await user— only blocking when it truly needs the value - Parallelize multiple parent fields —
const [u, o] = await Promise.all([user, org]) - Stay fully parallel — if it never awaits a parent field it runs concurrently with all others
The flat-merge of results into component props is unchanged — components always receive all data as plain values.
Typed Query Schemas
query belongs to createRoute(), not route.page().
import { t } from "elysia"
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"
export const route = createRoute({
parent: rootRoute,
query: t.Object({
page: t.Optional(t.Number({ default: 1 })),
tag: t.Optional(t.String()),
}),
})
import { route } from "./_route"
export default route.page({
loader: async ({ query }) => {
const posts = await db.getPosts(query)
return { posts }
},
component: ({ posts }) => <PostList posts={posts} />,
})
Typed Params
Params are also declared on createRoute().
import { t } from "elysia"
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"
export const route = createRoute({
parent: rootRoute,
params: t.Object({
slug: t.String(),
}),
})
Awaiting Parent Data
When a child loader depends on data returned by an ancestor, it awaits the relevant field(s).
Simple await
import { createRoute } from "@teyik0/furin/client"
import { route as dashboardRoute } from "../_route"
export const route = createRoute({
parent: dashboardRoute,
loader: async ({ user }) => {
// user is Promise<{ id: string; name: string }>
const { id } = await user
return {
posts: await getPostsForUser(id),
}
},
})
Parallel await with Promise.all
When a loader needs multiple parent fields, await them in parallel:
export const route = createRoute({
parent: teamRoute,
loader: async ({ user, org }) => {
// Start both Promises simultaneously
const [{ id }, { plan }] = await Promise.all([user, org])
return { canEdit: plan === "pro" || id === "admin" }
},
})
No await — fully parallel
A loader that doesn't need parent data at all starts immediately with zero waiting:
export default route.page({
loader: async ({ params }) => ({
analytics: await getAnalytics(params.id), // runs fully in parallel
}),
})
Redirects And Status
You can return a redirect from a loader.
export const route = createRoute({
loader: async ({ request, redirect }) => {
const user = await getSession(request)
if (!user) {
return redirect("/login", 302)
}
return { user }
},
})