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
| Primitive | Role |
|---|---|
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.