Skip to content
weftGitHub

Core

Async

Load data with resource, and say what to show while it loads with Boundary. On the server, weft can wait for that data so the first page arrives filled in.

A save button that waits 800 ms, standing in for a real request, and disables itself meanwhile.

ProfileSaves: 0
import { state } from '@weft-js/core'import { Badge, Button, Card, Spinner } from '@weft-js/warp'const save = () => new Promise(done => { setTimeout(done, 800) })export default function Profile() {  const saving = state(false)  const saves = state(0)  const go = async () => {    saving.value = true    await save()    saves.value++    saving.value = false  }  return (    <Card title="Profile">      <Badge tone="positive">Saves: {saves.value}</Badge>      <Button disabled={saving.value} onClick={go}>        {saving.value ? <Spinner /> : 'Save changes'}      </Button>    </Card>  )}
Try this
Press Save changes, and try pressing again while it spins.
Why it matters
Async code is plain async and await. State you change after an await updates the page like any other.
tsx
import { resource, Boundary } from '@weft-js/core'type User = { name: string }function Name({ id }: { id: string }) {  // `id` is read before the first `await`, so a new id fetches again and aborts the old request.  const user = resource(signal => fetch(`/api/users/${id}`, { signal }).then(r => r.json() as Promise<User>))  // `pending` is also true on a refetch, while the old value is still here: dim it rather than replace it (X033).  return <h1 aria-busy={user.pending ? 'true' : 'false'}>{user.value?.name}</h1>}// The resource is made inside the Boundary (in Name), so the Boundary waits for it.export function Profile({ id }: { id: string }) {  return (    <Boundary pending={<p>Loading…</p>} error={(e, retry) => <button onClick={retry}>Retry</button>}>      <Name id={id} />    </Boundary>  )}
  • resource(fetcher) is async derived: .value, .pending, .error, .refresh(). It re-fetches when what it read before the first await changes, and aborts the superseded fetch through signal. derived(async …) is W012.
  • <Boundary pending error> covers every resource created while its children are built, and every error they throw. A resource made above the <Boundary> in the same component isn't below it, and the fallback never shows (X037). The fallback shows after 150 ms and stays at least 400 ms; refetches keep the current content (no flash). error(e, retry) gets failed resources and children that throw during render.
  • @weft-js/core/ssr: renderToString(() => <App />) renders what it can now; await renderToStringAsync(() => <App />) waits for resources first and puts their values in the page. Pass { hydratable: true } when the client hydrates. renderToStream(() => <App />, { hydratable: true }) sends shell at once, then one chunk per <Boundary> as its resources resolve (chunks, an async iterable), and data() for hydrate when they're done. @weft-js/start streams this way for you.
  • hydrate(() => <App />, el) from @weft-js/core/dom takes over server HTML without rewriting it. Use it instead of render when the HTML came from @weft-js/core/ssr.
  • Import only renderToString, renderToStringAsync and renderToStream from @weft-js/core/ssr; its other exports are what compiled server code calls.