Data fetching
Three patterns. load() for server-rendered data. revalidate for cached data with background refresh. resource() for client-only fetches with reactive status.
load() — server-rendered
ts
// Server-only loader. Result is serialized into the SSR'd HTML
// and read back on the client at boot.
page('/posts/:id', {
load: async ({ params }) => ({
post: await db.posts.findOne(params.id),
}),
view: ({ post }) => <Article post={post} />,
})
load runs server-side; the return type is serialized to JSON and read back at client boot. Don't return classes or functions — they won't survive the round-trip.ISR — lazy stale-while-revalidate
ts
// Lazy stale-while-revalidate. The first request after `maxAge`
// returns the stale value and triggers a background regeneration.
page('/blog/:slug', {
load: async ({ params }) => ({ post: await fetchPost(params.slug) }),
revalidate: { maxAge: 60 * 1000 }, // 60 seconds
view: ({ post }) => <Article post={post} />,
})
After maxAge the cache is stale: the next request gets the stale value immediately, and a background revalidation refreshes it. Same shape as Next's ISR; no Vercel runtime required.
Typed URL params
ts
// Typed search params with shape() validation.
import { page, shape, useSearch } from '@place/component'
page('/posts', {
search: shape({ page: 'number', tag: 'string?' }),
view: (props) => {
const { page: p, tag } = useSearch<{ page: number; tag?: string }>(props)
return <PostList page={p} tag={tag} />
},
})
resource() — client-only
ts
// Client-side fetch with reactive status. Auto-disposes on unmount.
// The loader receives an AbortSignal — forward it so stale fetches
// are cancelled at the network layer.
import { resource } from '@place/reactivity'
const data = resource((signal) =>
fetch('/api/health', { signal }).then((r) => r.json()),
)
// The value read is the callable resource itself — data(), not
// data.value(). It returns the resolved value, or undefined while
// loading / on error. .loading() / .error() / .status() are the
// reactive status accessors.
<div>
{() => data.loading() && <Spinner />}
{() => data.error() && <p>{String(data.error())}</p>}
{() => data() && <Status v={data()} />}
</div>
// Or switch on the discriminated status — the cleanest shape:
{() => {
const s = data.status()
if (s.state === 'loading') return <Spinner />
if (s.state === 'error') return <p>{String(s.error)}</p>
return <Status v={s.value} />
}}
The Resource itself is the value accessor — call it (data()) to read the resolved value reactively; it returns undefined while loading or on error. Status lives on .loading(), .error(), and the discriminated .status(). There is no .value() method. Use for browser-only fetches that can't run on SSR.