defer
Marks loader or page server code data that can arrive after the first byte with render stream, with reactive state.
Se autoimporta en el código de servidor: server/ y <script server>.
import { defer } from "@meekojs/meeko/framework";defer(fábrica) or defer(promesa) marks slow page data. With render: "stream", the server sends HTML before the value resolves and the value arrives later in the same response; the page reads it with reactive status, value and error. With "ssr" or "prerender", the server awaits it before rendering, so HTML always contains it.
Use it for data that should not delay the first render: comments, recommendations, statistics. Critical data (the title, main content) goes without defer.
Usage#
<!-- app/pages/posts/[id].meeko -->
<script server lang="ts">
import { defer } from "@meekojs/meeko/framework";
export const route = definePage({
render: "stream",
load: ({ params }) => ({
post: { id: params.id ?? "", title: `Post ${params.id ?? ""}` },
comments: defer(async (signal) => {
const response = await fetch(`https://api.example.com/posts/${params.id ?? ""}/comments`, { signal });
return (await response.json()) as { id: string; text: string }[];
}, { timeoutMs: 5_000 }),
}),
});
</script>
<script lang="ts">
import type { Deferred } from "@meekojs/meeko/framework";
interface Comment { id: string; text: string }
export interface Props {
params: { id: string };
search: Record<string, string | string[]>;
data: { post: { id: string; title: string }; comments: Deferred<Comment[]> };
}
const props = defineProps<Props>();
</script>
<template>
<h1>{props.data.post.title}</h1>
<p if:when="props.data.comments.status === 'pending'">Loading comments…</p>
<p if:when="props.data.comments.status === 'rejected'" role="alert">Could not load ({props.data.comments.error?.code}).</p>
<ul if:when="props.data.comments.status === 'resolved'">
<li each:of="props.data.comments.value ?? []" each:as="comment" each:key="comment.id">{comment.text}</li>
</ul>
</template>The title arrives in the first byte; comments arrive when the API responds or after 5 seconds. Since 1.7, defer is auto-imported in <script server> (including route options), *.route.ts sidecars and server/; previously you had to import it from @meekojs/meeko/framework, and explicit imports still work.
Type#
function defer<T>(factory: DeferredFactory<T>, options?: DeferOptions): Deferred<T>function defer<T>(promise: PromiseLike<T> | T, options?: DeferOptions): Deferred<T>interface Deferred<T> {
readonly status: DeferredStatus
readonly value: T | undefined
readonly error: DeferredError | undefined
}interface DeferOptions {
readonly timeoutMs?: number
}interface DeferredError {
readonly code: string
readonly status: number
}Parameters#
| Parameter | Type | Default | Description |
|---|---|---|---|
source |
(signal: AbortSignal) => T | PromiseLike<T>, or a promise |
required | A factory Meeko calls when the loader or block finishes, or an already started promise. The factory receives an AbortSignal aborted when the value is no longer awaited: timeout, client disconnect, session revocation, canceled stream, HEAD request or, in the browser, leaving the route. |
options.timeoutMs |
number |
10000 |
Timeout in milliseconds, from when the loader returns. Must be finite and positive (otherwise, RangeError). |
Prefer the factory: an already started promise cannot be canceled, only no longer awaited.
Return value#
A Deferred<T>:
| Property | Type | Description |
|---|---|---|
status |
"pending" | "resolved" | "rejected" |
The state. May gain members in a minor release: an exhaustive switch needs a default branch. |
value |
T | undefined |
The resolved value, or undefined until resolved. |
error |
{ code, status } | undefined |
Public failure if rejected: an ApiError retains its code and status; any other error arrives as INTERNAL_ERROR/500; a timeout as DEFERRED_TIMEOUT/504. |
Reading status, value or error in a template, computed or effect subscribes to the result.
Examples#
With a loader#
// app/pages/dashboard.route.ts
import { defer, definePage } from "@meekojs/meeko/framework";
export const route = definePage({
render: "stream",
load: async () => ({
user: { name: "Ana" },
stats: defer(async (signal) => {
const response = await fetch("https://api.example.com/stats", { signal });
return (await response.json()) as { visits: number };
}),
}),
});The page receives props.data.stats as Deferred<{ visits: number }>.
In top-level code#
An export const with defer in <script server> reaches the template under the same name:
<!-- app/pages/home.meeko -->
<script server lang="ts">
export const route = definePage({ render: "stream" });
export const hero = { title: "Hello" };
export const weather = defer(async () => ({ city: "Lima", celsius: 19 }), { timeoutMs: 2_000 });
</script>
<template>
<h1>{hero.title}</h1>
<p if:when="weather.status === 'pending'">Checking the weather…</p>
<p if:when="weather.status === 'resolved'">{weather.value?.city}: {weather.value?.celsius} °C</p>
</template>Each defer arrives when it resolves, in any order.
The template receives weather as Deferred<{ city: string; celsius: number }>, with its types: list items in value have the list's type (each:of="feed.value ?? []"). Through 1.6, defer was not auto-imported in <script server>, and without an import the export arrived untyped (value.value compiled and items were unknown).
Common mistakes#
Read the value as a signal#
Deferred is not a signal: read status, value and error directly, and value may be undefined.
<p>{props.data.stats.value.value.visits}</p>With stats: Deferred<{ visits: number }> in Props, meeko check reports TS18048: 'props.data.stats.value' is possibly 'undefined'. and TS2339: Property 'value' does not exist on type '{ visits: number; }'.
<p if:when="props.data.stats.status === 'resolved'">{props.data.stats.value?.visits} visits</p>An invalid timeoutMs#
const slow = defer(load, { timeoutMs: 0 });At runtime, defer throws RangeError: defer timeoutMs must be a positive finite number of milliseconds.
const slow = defer(load, { timeoutMs: 5_000 });Expect defer to speed up an "ssr" page#
With render: "ssr" or "prerender", the server awaits all defer values before responding: the page does not arrive earlier, without any warning.
export const route = definePage({ load: async () => ({ comments: defer(loadComments) }) });export const route = definePage({ render: "stream", load: async () => ({ comments: defer(loadComments) }) });Notes#
- Streaming. With
"stream", middleware, validation, the critical loader,headand synchronous rendering run before the first byte. A<script type="application/json" data-meeko-deferred="N">then arrives for each resolved value, applied by the browser after hydration. - Navigation. When navigating to a
"stream"route, the browser receivesapplication/meeko+ndjson: the payload first and each value afterward. Leaving the route aborts pending factories. - Security. A private error never reaches the browser: only
codeandstatus.5xxerrors reach the server'sonErrorwithrequestId. - Prerendering. With
render: "prerender", a rejecteddeferfails the request with500andmeeko buildwith the route, instead of saving the error in HTML. - Cache (
1.7). WithcacheindefinePage, a"stream"page only usesETagand304: its body is not cached in memory, because that would fix its deferred values. - Serialization. The resolved value travels as JSON, like any page data.