Overview

paginatedAsyncAction extends asyncAction with a dedicated page cursor and helpers like next(). You get the full reactive state machine (isLoading, data, error, status) plus page and totalPages atoms.

The async function receives no arguments — read fetchPosts.page.val (the non-reactive snapshot) inside the fn to build the request URL without registering the page atom as a reactive dependency. Set totalPages from the response so Prev/Next buttons can disable at the boundaries.

vs asyncAction

Capability
asyncAction
paginatedAsyncAction
Reactive state machine data / isLoading / error / status Same atoms, same semantics.
Page cursor .page() atom (starts at 1) + writable .page.set().
Total pages .totalPages() — set inside fn from the API response.
Next helper .next() increments page and re-fetches in one call.

paginatedAsyncAction — jsonplaceholder users

Reads fetchUsers.page.val inside the fn, sets totalPages from X-Total-Count, and wires abortController. next() advances; Prev uses page.set then a call. next() is a no-op on the last page.

import { html, nothing, renderApp } from 'mates';
import { paginatedAsyncAction } from 'mates';

const LIMIT = 5;

const fetchUsers = paginatedAsyncAction(async () => {
const page = fetchUsers.page.val; // snapshot — do not call page() inside fn
const res = await fetch(
`https://jsonplaceholder.typicode.com/users?_page=${page}&_limit=${LIMIT}`,
{ signal: fetchUsers.abortController.signal },
);
if (!res.ok) throw new Error('Failed to load users');
const total = Number(res.headers.get('X-Total-Count') || 10);
fetchUsers.totalPages.set(Math.ceil(total / LIMIT));
return res.json();
});

fetchUsers(); // page 1

const App = () => {
return () => html`
<div class="m-col m-gap">
<p class="muted">status=${fetchUsers.status()} · page=${fetchUsers.page()} / ${fetchUsers.totalPages()}</p>
${fetchUsers.isLoading()
? html`<p class="muted">Loading page ${fetchUsers.page()}…</p>`
: fetchUsers.error()
? html`<p>${fetchUsers.error().message}</p>`
: fetchUsers.data()?.map(user => html`
<div class="card card-body m-flex m-gap-sm m-items-center m-justify-between">
<span>${user.name}</span>
<span class="tag">${user.email}</span>
</div>
`)
}
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-ghost"