From one
Server-side data loading in One framework. Use when fetching data for pages, implementing caching, redirects, response headers, ISR, refetching, or file-driven content with loaders.
How this skill is triggered — by the user, by Claude, or both
Slash command
/one:one-loadersThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Official docs: [Loaders](https://onestack.dev/docs/routing-loader)
Official docs: Loaders
Loaders are server-side data fetching functions that run before rendering. They are tree-shaken from the client bundle — database queries, API keys, and server logic never reach the browser.
import { useLoader } from 'one'
export async function loader({ params }) {
const post = await db.posts.findUnique({ where: { slug: params.slug } })
return { post }
}
export default function PostPage() {
const { post } = useLoader(loader)
return <Text>{post.title}</Text>
}
export async function loader({ params, path, request }) {
// params — dynamic route segments ({ slug: 'hello' })
// path — full pathname ('/blog/hello')
// request — Web API Request (SSR only, undefined in SSG)
}
Return any JSON-serializable value:
return { user, posts } // object (most common)
return posts // array
return Response.json({ data }) // Response object
Loaders automatically refetch when:
/home → /about)/user/1 → /user/2)?q=hello → ?q=world)| Feature | useLoader | useLoaderState |
|---|---|---|
| Returns data | data | { data, refetch, state } |
| Manual refetch | No | Yes |
| Loading state | No | 'idle' or 'loading' |
| Works without loader arg | No | Yes (access from anywhere) |
import { useLoaderState } from 'one'
// with loader — replaces useLoader
export default function Page() {
const { data, refetch, state } = useLoaderState(loader)
return (
<>
{state === 'loading' && <Spinner />}
<Content data={data} />
<Button onPress={refetch}>Refresh</Button>
</>
)
}
All useLoaderState hooks on the same route share state. Call refetch() from a child component — all subscribers update:
// in a header button, sidebar, or any child component
function RefreshButton() {
const { refetch, state } = useLoaderState()
return (
<Button onPress={refetch} disabled={state === 'loading'}>
Refresh
</Button>
)
}
Pull-to-refresh:
const { refetch, state } = useLoaderState()
<PullToRefresh onRefresh={refetch} refreshing={state === 'loading'}>
{children}
</PullToRefresh>
Polling:
const { refetch, state } = useLoaderState(loader)
useEffect(() => {
const interval = setInterval(() => {
if (state === 'idle') refetch()
}, 5000)
return () => clearInterval(interval)
}, [refetch, state])
Form revalidation:
const { refetch } = useLoaderState()
const handleSubmit = async (data) => {
await submitForm(data)
refetch() // reload page data after mutation
}
Throw a redirect to prevent unauthorized content from ever reaching the client:
import { redirect } from 'one'
export async function loader({ request }) {
const user = await getUser(request)
if (!user) {
throw redirect('/login')
}
return { user }
}
Both throw redirect() and return redirect() work. throw stops execution immediately.
How it works during client-side navigation: The server detects the redirect, transforms it into a JS module with redirect metadata (not a raw 302), and the client intercepts it before rendering — no sensitive data reaches the client.
Set caching, cookies, or custom headers:
import { setResponseHeaders } from 'one'
export async function loader() {
await setResponseHeaders((headers) => {
headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
})
return fetchData()
}
Common cache patterns:
s-maxage=3600 — CDN caches for 1 hourstale-while-revalidate=86400 — serve stale while revalidating (up to 1 day)max-age=0, must-revalidate — always revalidate with originprivate, no-store — never cache (user-specific data)Note: stale-while-revalidate requires CDN support (Vercel, CloudFront, Fastly). Cloudflare does not currently support it.
await setResponseHeaders((headers) => {
headers.append('Set-Cookie', `session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`)
})
Deduplicate concurrent SSR calls:
export function loaderCache(params, request) {
return {
key: `post-${params.slug}`,
ttl: 60, // seconds
}
}
Register a file dependency — when it changes, the loader re-runs and data refreshes without full page reload:
import { watchFile } from 'one'
import { readFile } from 'fs/promises'
export async function loader({ params }) {
const filePath = `./content/${params.slug}.mdx`
watchFile(filePath)
return { content: await readFile(filePath, 'utf-8') }
}
No-op in production and on the client.
Validate params before loading:
// schema validation
export function validateParams(params) {
return { slug: z.string().parse(params.slug) }
}
// async validation (check record exists)
export async function validateRoute({ params }) {
const exists = await db.posts.exists({ slug: params.slug })
if (!exists) return false // renders +not-found
return true
}
Layouts can export loaders. Access layout data from pages via useMatches():
// app/_layout.tsx
export async function loader() {
return { user: await getUser() }
}
// app/index.tsx — child page
import { useMatches } from 'one'
const matches = useMatches()
const layoutData = matches[0]?.loaderData // parent layout data
For SSG pages with dynamic routes, export generateStaticParams:
// app/blog/[slug]+ssg.tsx
export async function generateStaticParams() {
const posts = await db.posts.findMany()
return posts.map(post => ({ slug: post.slug }))
}
export async function loader({ params }) {
return db.posts.find(params.slug)
}
request objectuseLoaderState refetch works client-sideWrong: Importing server code at the top level of a component file.
// BAD — leaks to client
import { db } from './database'
Right: Keep server code inside the loader:
export async function loader() {
const { db } = await import('./database')
return db.query()
}
Wrong: Returning non-serializable values:
return { onClick: () => {}, createdAt: new Date() }
Right: Return JSON-serializable data:
return { createdAt: new Date().toISOString() }
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub onejs/skills --plugin one