Skip to content

Data fetching

Build a data-fetching query that retries on failure, pauses while offline, and refetches when the tab regains focus. Everything is composable reactive primitives β€” no query-library runtime.

The recipe

import { signal, effect, untracked, onCleanup } from "elements-kit/signals";
import { async } from "elements-kit/utilities/async";
import { retry } from "elements-kit/utilities/retry";
import { online } from "elements-kit/utilities/network";
import { windowFocused } from "elements-kit/utilities/window-focus";
import { createLocalStorage } from "elements-kit/utilities/storage";
const id = signal(1);
const cache = createLocalStorage<unknown>("todo-cache", null);
const fetchTodo = async(() => {
if (!online()) return untracked(cache); // pause while offline, return stale value
windowFocused(); // refetch on tab focus
return retry(() => {
const controller = new AbortController();
onCleanup(() => controller.abort()); // abort before each retry
return fetch(`/api/todos/${id()}`, { signal: controller.signal })
.then((r) => r.json())
.then((value) => (cache(value), value));
}, 3, (n) => n * 500)(); // 0 ms β†’ 500 ms β†’ 1000 ms backoff
}).start();
effect(() => console.log(fetchTodo.state, fetchTodo.value));

How each piece contributes

PrimitiveRole
async(…).start()Reactive controller. Reruns the body whenever a tracked signal changes.
online()Pauses the fetch and returns the cached value while offline.
windowFocused()Refetches when the user returns to the tab.
retry(fn, 3, …)Retries on rejection with exponential backoff.
onCleanup(abort)Cancels the in-flight request before each retry or re-run.
createLocalStorage(…)Persists the last-known-good value across reloads.

Variations

One-shot mutation β€” use .run() with an argument. Untracked by default:

const deleteTodo = async((todoId: number) =>
fetch(`/api/todos/${todoId}`, { method: "DELETE" }).then((r) => r.json()),
);
await deleteTodo.run(42);

Interval polling β€” read a timestamp signal inside the body:

import { createInterval } from "elements-kit/utilities/interval";
const timer = createInterval(10_000);
const poll = async(() => {
timer.timestamp(); // re-runs every 10s
return fetch("/api/status").then((r) => r.json());
}).start();

Drop the cache β€” createLocalStorage is optional. The body can return undefined while offline if stale values aren’t useful.

Gotchas

See also

  • Async β€” full async / Async reference.
  • Promise β€” the underlying reactive-promise primitive.
  • Signals β€” onCleanup, untracked.
  • Utilities β€” online, windowFocused, createLocalStorage, retry, createInterval.