brumate

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.

App.brmEdit the code to run it again

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()));

#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:

App.brmEdit the code to run it again

The sample runs when it comes into view.

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> }
}

$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 client

When a value that the memo reads changes in the browser, the memo runs again:

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>

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.

App.brmEdit the code to run it again

The sample runs when it comes into view.

Promises as props

A component can pass a promise to another component as a prop. The child loads it with a memo:

Edit the code to run it again

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:

App.brmEdit the code to run it again

The sample runs when it comes into view.

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>

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>
}

On the server

The server renders the page once, so it waits for the values that the page needs:

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.

#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.

#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.