Async data
A $memo can hold a promise, such as the result of a server function. The memo gives the value that the promise settles to, so markup and other memos never see the promise.
The sample runs when it comes into view.
search is a server function: it runs on the server, and the browser calls it over the network. Type in the field to see the old results stay while new ones load.
Loading a value
Any expression that gives a promise works in a memo: a server function, fetch, or an imported async function.
const user = $memo(getUser(id));
const posts = $memo(fetch(`/api/posts?user=${id}`).then((response) => response.json()));
- A memo that reads a loading memo gets its value, never a promise. While the value loads, that memo waits too.
awaitworks in the expression:$memo(await load(id)).- The type of the memo is the value, not the promise.
$memo(getUser(id))has the type of the user.
#loading
#loading renders its #else body first, and its own body once the values it reads have loaded. The rest of the page does not wait for them:
The sample runs when it comes into view.
- The heading shows at once. The quote streams in after a second and a half.
- Without
#loading, the whole page waits for the quote. - When the values that the body reads are ready as the block renders, such as a value the page waited for with
await, the body renders in place: no fallback, no swap. A body that renders a component, a dynamic tag, a template or a prop always streams, because those can wait on their own. - On the client, a
#loadingbody keeps its content while a new value loads. Use$isPendingto show that it is loading again.
Reveal order
#loading bodies show as their data arrives, in any order, so a page with several of them can jump around while it streams. #reveal shows them in an order:
#reveal ('sequential', collapsed) {
#loading { <Profile /> } #else { <p>Loading the profile…</p> }
#loading { <Posts /> } #else { <p>Loading the posts…</p> }
#loading { <Friends /> } #else { <p>Loading the friends…</p> }
}
'sequential', the default, shows the bodies in the order of the markup. A body that is ready waits until every body before it shows.'backwards'shows them from the last to the first.'together'shows every body at once, when the last one is ready.'natural'shows each body when it is ready, as without#reveal.collapsed, with'sequential', shows only the fallback of the next body to show. The others appear one at a time, as the body before each shows.- The data of every body still loads at once. Only the showing waits, on the server, so it needs no code in the browser.
- A body that fails keeps its fallback and takes its turn, so the bodies after it still show.
- It orders the
#loadingblocks in its markup, and those of the components that it renders, such as a list of cards that each load their own data. - A
#revealinside a#loadingbody orders the bodies of that body. Markup that the browser renders has no stream, so there#revealrenders its children as they are.#deferblocks inside it are not ordered.
$isPending
$isPending(memo) is true while a new value of the memo loads on the client. It is also true while a memo that it reads loads.
const results = $memo(search(query));
const count = $memo(results.length);
// True while `results` loads, because `count` reads it.
const busy = $isPending(count);
- On the server it is always false, because the server waits for the value before it renders.
- Pass the memo itself, not an expression.
On the client
When a value that the memo reads changes in the browser, the memo runs again:
- The page keeps the old value until the new one arrives, so content does not disappear while it loads.
- Only the latest run is written. When you type quickly, an older answer that arrives late is dropped.
- A newer run aborts the server function calls of the older one. A server function sees this on the request, with
getEvent().signal. - If the promise rejects, the memo holds the error until it computes again, and each read of it throws. A
#tryaround the read renders its#catchbody, and code that does not read the memo runs as usual. See Errors in the browser. The function can also return{ error }when the page should show the failure as a value.
Waiting for a pause
A search field that loads results as the user types should wait for a pause. $debounced(value, ms) is a memo that takes the value once it stops changing for ms milliseconds:
<script>
import { $debounced, $isPending, $memo, $signal } from 'brumate/signals';
import { search } from './search.ts';
let query = $signal('');
const settled = $debounced(query, 300);
const results = $memo(search(settled));
</script>
<input type="search" value=query onInput=((event) => (query = event.currentTarget.value))>
#if ($isPending(settled)) {
<p>Waiting for you to stop typing…</p>
}
<ul>
#for (result of results by result.id) {
<li>{result.title}</li>
}
</ul>
$throttled(value, ms)takes the value at most once everymsmilliseconds: the first change at once, and the last one after the wait. Use it for values that change all the time, such as the position of the pointer.- On the server, both are the value itself, so the page renders the results for the query that it has.
- In the browser, the memo is pending while it waits, so
$isPendingis true. msis a number, such as300.
Loading again
A memo that calls a server function loads again when the values it reads change. When another server function changes the data, call event.refresh with the function that reads it. Every memo on the page that calls that function loads again.
The sample runs when it comes into view.
event.refreshtakes a server function of the same component, or a function of a'use server'file.- The memos load again in every component on the page that calls the function.
Promises as props
A component can pass a promise to another component as a prop. The child loads it with a memo:
The sample runs when it comes into view.
When the parent passes a new promise, the child keeps the old value until it settles, and $isPending(profile) is true meanwhile.
Streams
A server function can be an async generator. A memo of it takes each value as it arrives:
The sample runs when it comes into view.
- The memo is pending until the first value arrives, then shows each value in turn.
- A newer run of the memo stops reading the older stream.
- The server renders the first value only, because the page renders once.
$memogives the type of the values, sostatusabove is astring.
Lists that grow
gather from brumate/stream turns a stream into the list of its values so far, such as the tokens of a reply, search results or log lines. A memo of it renders with #for:
<script>
import { $memo, $signal } from 'brumate/signals';
import { gather } from 'brumate/stream';
import { reply } from '../chat.ts';
let prompt = $signal('');
const tokens = $memo(gather(reply(prompt), { key: (token) => token.id }));
</script>
<ol>
#for (token of tokens by token.id) {
<li>{token.text}</li>
} #else {
<li>Ask something</li>
}
</ol>
- The list starts empty, and grows by one item for each value of the stream.
keyputs a value with a key that came before in place of its item, so a stream can update an item.- A new stream, such as for a new prompt, starts the list again, and the memo stops reading the older stream.
- The empty list comes before the stream is read. The server renders the first value of a memo, so it renders the empty list and never starts the stream. The list fills in the browser, when the memo runs there, such as after the prompt changes.
await
await says where the server waits instead of streaming. A read with await gives the value, never a promise and never a fallback:
<script>
import { $memo, $props } from 'brumate/signals';
import { fetchSettings, fetchUser, listTags } from './data.server';
const { id } = $props<{ id: string }>();
// The server waits here. A constant: it does not load again in the browser.
const settings = await fetchSettings();
// A memo that the page waits for. It loads again when `id` changes.
const user = await $memo(fetchUser(id));
// A memo that streams, unless an `await` reads it.
const tags = $memo(listTags());
</script>
<p>{settings.theme}</p>
#loading {
<!-- The page waits for the user. The tags can show the fallback. -->
<h1>{user.name}</h1>
<p>{tags.join(', ')}</p>
} #else {
<p>Loading…</p>
}
- In a script,
awaitof a promise works as in JavaScript. The script runs on the server only, so the value is a constant. A component whose script awaits a promise renders only on the server: the browser throws when it renders the component, such as inside a block of a parent that changes. const name = await $memo(value)declares a memo that the page waits for where it is declared. In the browser it is a memo like any other: it loads again when what it reads changes.await nameof a memo, in a script, an insert, an attribute, a prop or a block header, waits for the memo in the render around it, and never shows a fallback for it. Outside every#loadingbody, the page waits. Inside a#loadingbody, the render around the block waits before it reaches the block: the page, or the body of a#loadingaround it. Inside a#deferbody, the request for the body waits.- In the browser, the value of a memo is settled when the code that reads it runs, so
await nameis a plain read. Code that the browser renders or updates, such as a branch of an#ifthat changes, can await nothing other than a memo. The compiler reports it in the same file. - A run of
await $memo(…)declarations, one after another, starts every memo of the run, then waits for all of them where the run ends: before the next statement, and before any markup. Independent memos load at the same time. Another statement ends the run, so code between two declarations sees the first memo settled, as it would in JavaScript. A memo that reads another one waits for it by itself, so inconst a = await $memo(foo()); const b = await $memo(bar(a)),bar(a)runs whenfoo()settles. When a memo fails, the error comes where the run waits.
On the server
The server renders the page once, so it waits for the values that the page needs:
- Markup outside every
#loadingblock waits for the memos it reads. The page starts to stream after they settle. - Markup inside a
#loadingblock does not hold up the page. When the values that the body reads are ready as the block renders, the body renders in place. Otherwise the fallback goes out with the first render, and the body streams in when its values settle. - A page that waits for every value it loads has one state record, and nothing streams after it.
Pages for readers without scripts
A #loading body streams in a <template> that a script swaps in. A reader that runs no scripts, such as a crawler, would keep the fallback. For such a reader, the server renders every #loading and #defer body in place, in the order of the page: it waits for each body and writes it where the block is, with no <template> and no swap. The page arrives later, and complete, and resumes as it does when it streams.
inOrderin the options ofhandle,renderorrenderToStreamdecides.'bots', the default, does it for requests whoseUser-Agentis a known crawler or link preview.truedoes it for every request,falsefor none, and a function of the request decides for each.- A
#deferbody renders with the page, since a crawler does not fire its trigger. - A
#clientbody has no server markup, so a crawler sees its#elsebody.
#defer
#defer renders its body on the server later, in a request of its own. The page has the #else body until the trigger fires, with the same triggers as #client. The browser then asks the server for the body, which replaces the fallback:
#defer ('visible') {
<Recommendations user=user />
} #else {
<p>Loading recommendations…</p>
}
Use it for markup that the server renders but that the first response does not need, such as content far below the fold, or the parts of a cached page that depend on the user.
- The body renders on the server, with the same route, request, cookies and values as the page. It can call server functions and read the database.
- The request goes to the URL of the page, so middleware runs for it as it does for the page. The answer has
vary: x-brumate-defer, and so does the page. - The server renders the route again for the request, up to the block. It renders the
#loadingand#deferbodies around the block, without sending them, and no other body. Data that the page loads before the block loads again. Data that only the body needs is not loaded for the page: put it in the body, such as in a component inside it. - If the page's data changed since it loaded, and the block is not where the browser has it, the fallback stays.
- Without scripts, the fallback stays. A prerendered page needs a server for the request.
- A
#deferblock can be inside a#loadingor#deferbody. It cannot be inside a#clientbody, or in markup that the browser renders, such as a branch of an#ifthat changes. The compiler reports it in the same file. - A component with a
#deferblock that the browser renders, such as inside the#clientbody of its parent, keeps the fallback. The compiler cannot see this across files.
#client renders its body in the browser, for markup that needs the browser. #defer renders it on the server, for markup that needs the server.