Wisp is in pre-release, self-hosted since GitHub suspended us. Install it, or see the changelog.

Wisp

Fetching Data in the Browser

swr

swr(key, fetcher, options) keeps fetched data in a cache. Browser code gets the data, and the page draws again when it changes. The name is short for stale-while-revalidate: the page shows old data while the cache fetches new data.

<script>
  const todos = swr('todos', ({ signal }) => fetch('/api/todos', { signal }).then((r) => r.json()))
</script>
{:#if todos.loading}<p>Loading</p>{:/if}
{:#each todos.value ?? [] as t (t.id)}<li>{:t.title}</li>{:/each}

Only a module that names swr loads /_app/c/swr.js. A page that does not name it pays nothing for it.

The fetcher is any function that returns a value or a promise. It gets { signal, key }. Pass signal to fetch so the request stops when nothing reads the data.

The value

NameMeaning
todos.valueThe data, or undefined before the first answer.
todos.errorThe last failure. It stays until an answer comes. The data stays too.
todos.loadingtrue until the first answer or failure.
todos.fetchingtrue while a request is out.
todos.refresh()Fetches now, or joins the request in flight. It returns a promise of the data. It rejects on failure. When the request is dropped (the entry sleeps, or mutate or swr.set writes), the promise resolves with the data the entry has then.
todos.mutate(v)Writes the data now and fetches nothing. v can be a function of the old data.
todos.invalidate()Marks the data stale, and fetches again if something reads it.

Keys

The key is a string, an array, or a function that returns one. A function key can read state. The query follows it:

let id = $state(1)
const user = swr(() => ['user', id], ({ key }) => fetch(`/api/users/${key[1]}`).then((r) => r.json()))

A key of null, undefined, false or '' switches the query off (value is undefined, loading is false). swr.invalidate with such a key marks nothing stale. Only a call with no argument marks all entries. Calls with the same key share one entry and one request, also in other components. The newest call sets the fetcher and the options of the entry, except interval: the entry uses the shortest interval of its callers while it is awake.

Objects in a key compare by content and not by the order of their names: ['k', { a: 1, b: 2 }] and ['k', { b: 2, a: 1 }] are one key. undefined, NaN, Infinity and a bigint are keys of their own. A Set and a Map compare by their entries, in the order they hold them: new Set([1]) and new Set([2]) are two keys. A key with a cycle does not throw. The query shows a TypeError ("the key has a cycle") as error, and nothing is fetched.

When it fetches

An entry is awake while a block or $effect reads one of its values. When the last reader ends (the page changed, a block left), the entry sleeps. A sleeping entry fetches nothing, aborts its request, and keeps its data for gc ms. A page that comes back shows that data at once.

An awake entry fetches when:

  • it has no data, or the data is older than stale,
  • the tab shows again, the window gets focus or the network returns, and the data is stale or the last request failed,
  • interval ms pass after the last answer (a fetch that a focus, a network return or refresh() starts moves the next one), while the tab is shown and the browser is not offline (navigator.onLine is not false). An interval tick that falls in a hidden or offline period is skipped.

Options:

OptionDefaultMeaning
initialnoneData that counts as fresh: a value, or key => value. It seeds an empty entry only.
stale30000Age in ms after which data is stale. The same number as data-wisp-revalidate.
intervalnoneFetch again every N ms while the entry is awake and the tab is shown.
gc300000Time in ms that a sleeping entry keeps its data. Infinity keeps it for good.

Seeds from the server

A page's server values are browser values by name. Pass one as initial and the first draw shows it with no request:

---
let notes: Vec<Note> = Note::all();
---
<script>
  const list = swr('notes', () => fetch('/api/notes').then((r) => r.json()), { initial: notes })
</script>

The seed counts as fresh for stale ms. After that, the next focus or the next read fetches.

Invalidation

swr.invalidate(key) marks the entries stale. The key is a prefix: swr.invalidate('todo') stales 'todo', ['todo', 1] and ['todo', 2]. swr.invalidate(['todo', 1]) stales that key and the keys below it. With no key it stales all entries. The entries that something reads fetch again at once. The call returns a promise that resolves when they have, and never rejects.

A form that posted with an ok answer (wisp:result) stales all entries, so a form needs no code for it. So does the page-wide invalidate() with no key (wisp:refresh, also after each rebuild of wisp dev).

swr.set(key, v) writes the data of a key now, with no request. v can be a function of the old data. An optimistic update writes first and checks the server after:

async function done(t) {
  swr.set('todos', (all) => all.map((x) => (x.id === t.id ? { ...x, done: true } : x)))
  await fetch(`/api/todos/${t.id}`, { method: 'PATCH', body: '{"done":true}' })
  swr.invalidate('todos')
}

With #[remote] queries

A #[remote(get)] function already is a cache entry: calls with the same arguments share one request, and a command or a form refreshes it (Server functions and errors). Use swr for data that comes from other sources: an API route of the app, another site, or a fetch of your own.

Limits

  • The cache lives in the page. A full load of a page starts it empty.
  • A failed request does not repeat by itself. The next focus, network return, interval tick or refresh() repeats it. A reader that wakes a sleeping entry with an error repeats it too.
  • swr.set(key, v) on a key with no entry creates the entry. A reader that comes later finds the data. An entry that nobody reads is deleted after gc ms.
  • The key must be JSON data. A function, a Symbol or a class instance in a key is compared by its JSON form.
  • A fetcher that ignores signal still runs to its end after the entry sleeps. Its answer is not stored.

Is This Page Useful?

View This Page's Source

Type to search every docs page and heading.