<!--
Sitemap:
- [rxfy](/index): Typed, normalized, reactive state — built on RxJS
- [Comparison](/comparison): rxfy versus Redux Toolkit, MobX, Jotai, TanStack Query, and TanStack DB
- [Inspired by](/inspired-by): the libraries and ideas rxfy grew out of
- [Agent Skills](/agent-skills): Accurate rxfy context for AI coding assistants
- [Examples](/examples): Runnable apps, from client-only to fully synced
- [Changelog](/changelog)
- [Getting Started](/getting-started)
- [Create Store](/getting-started/create-store): Normalized reactive state in a client-only app
- [Add SSR](/getting-started/add-ssr): Render the first paint on the server, hydrate with no refetch
- [Add Sync Client](/getting-started/add-sync-client): The full stack: the server publishes, the client syncs
- [Core Concepts](/core-concepts): The ideas rxfy is built on
- [Observables](/core-concepts/observables): A value that changes over time, that you can subscribe to
- [Normalization](/core-concepts/normalization): Store each entity once, reference it by id
- [Late Unwrapping](/core-concepts/late-unwrapping): Unwrap async state at the leaf, not the trunk
- [Server-Side Rendering](/core-concepts/ssr): Dehydrate on the server, hydrate with no refetch
- [rxfy](/rxfy): The core package: atoms, lenses, models, and states
- [createModel](/rxfy/create-model): Typed entities in normalized storage
- [defineState](/rxfy/define-state): Typed, normalized state shapes with fetching and mutations
- [createAtom](/rxfy/create-atom): A reactive cell with synchronous get, set, and modify
- [createLens](/rxfy/create-lens): A two-way view into part of an Atom
- [React Bindings](/react): Hooks and helpers for using rxfy in React
- [useStateData](/react/use-state-data): Fetch, normalize, and subscribe to a query
- [useStatePagedData](/react/use-state-paged-data): Paginated and infinite-scroll lists
- [useModelStore](/react/use-model-store): Subscribe to one normalized entity by id
- [useAtom](/react/use-atom): Two-way binding for any IAtom
- [Pending](/react/pending): Render pending, rejected, and fulfilled UI for any observable
- [usePending](/react/use-pending): The status value behind Pending
- [useObservable](/react/use-observable): Bind a raw Observable to React
- [Sync Client in React](/react/sync-client): StoreProvider, useSyncClient, and update handles
- [rxfy-client](/framework/client): The framework-agnostic browser sync runtime
- [createSyncClient](/framework/client/create-sync-client): Connect a transport and drive the sync loop
- [readSsrGrants](/framework/client/read-ssr-grants): Lift SSR-embedded channel grants
- [rxfy-server](/framework/server): Bind Drizzle tables, write, and publish sync updates
- [defineResource](/framework/server/define-resource): Tie a Drizzle table to an rxfy model
- [createSync](/framework/server/create-server): Wire a storage adapter, hub, and secret into a Live object
- [createInMemoryHub](/framework/server/hub): The socket-keyed pub/sub backbone
- [Writes](/framework/server/writes): sync.create / sync.update / sync.delete and touch
- [Storage adapters](/framework/server/storage-adapters): Persist writes with Drizzle or in memory
- [Sync messages](/framework/server/messages): What travels between server and client
- [Grants](/framework/server/grants): The server signs what it serves; the client subscribes with the token
- [rxfy-ws](/framework/ws): The default WebSocket transport
- [createWsServer](/framework/ws/server): Attach a Hub to WebSocket connections
- [createWsClient](/framework/ws/client): The browser transport with reconnect and replay
- [Custom transports](/framework/ws/custom-transport): Bring your own ClientTransport
- [Guides](/guides): Task-focused walkthroughs of common rxfy patterns
- [Pagination and infinite scroll](/guides/pagination): Load and append pages into one normalized list
-->

# useStatePagedData \[Paginated and infinite-scroll lists]

A focused helper for paginated / infinite-scroll lists of **one entity type**. You give it a
`model` (the list is always `array(model)`), not a full state — so `data$` emits a flat `string[]`
of ids. Page 0 is fetched, cached, and SSR'd through [`useStateData`](/react/use-state-data);
`loadMore()` fetches the next page and appends it into the same growing list. Each page does
O(page-size) work — the rows already loaded are never re-normalized, so scrolling stays linear.

You supply three callbacks: `fetchPage` (fetch one page for a cursor), `getCursor` (compute the
next cursor from the current `ids: string[]` and running `pageIndex`), and `select` (the entities
a page contributes — appended to the list).

```tsx
import { useMemo } from "react";
import { useStatePagedData, Pending } from "rxfy-react";
import { userModel, fetchUsers } from "./users";

function Users() {
  const params = useMemo(() => ({}), []);

  const { data$, loadMore, isLoading, hasMore } = useStatePagedData({
    model: userModel,
    key: "users", // SSR / query-cache key
    params,
    fetchPage: ({ cursor }) => fetchUsers(cursor === 0 ? null : String(cursor)),
    getCursor: ({ ids }) => ids.length, // offset cursor = rows already loaded
    select: ({ page }) => page.items, // the new rows this page adds
    hasMore: ({ page }) => page.items.length > 0, // omit for an endless list
  });

  return (
    <Pending value$={data$} pending={<p>Loading…</p>}>
      {(ids) => (
        <>
          <ul>
            {ids.map((id) => (
              <UserRow key={id} id={id} />
            ))}
          </ul>
          {hasMore && (
            <button onClick={() => loadMore()} disabled={isLoading}>
              {isLoading ? "Loading…" : "Load more"}
            </button>
          )}
        </>
      )}
    </Pending>
  );
}
```

```ts
function useStatePagedData<T, TParams, TPage, TCursor>(config: {
  model: ModelDescriptor<T>;
  key?: string; // SSR / cache key; omit to fetch per mount
  params: TParams;
  fetchPage: (args: { cursor: TCursor; params: TParams; signal: AbortSignal }) => Promise<TPage>;
  getCursor: (args: { ids: string[]; pageIndex: number }) => TCursor;
  select: (args: { page: TPage }) => T[]; // the page's new entities
  hasMore?: (args: { page: TPage }) => boolean; // omit ⇒ infinite
}): {
  data$: Observable<string[]>; // entity ids — read entity data via useModelStore(model)
  loadMore: () => void;
  isLoading: boolean;
  hasMore: boolean;
  reload: () => void;
};
```

* **One model, treated as `array(model)`.** The hook is intentionally narrow — it pages a list of a
  single entity type. For arbitrary multi-field state shapes use [`useStateData`](/react/use-state-data)
  directly and append with `setRaw`.
* **Page 0 is `select(firstPage)`**, returned as the list shape, so it flows through `useStateData`'s
  cache / SSR / hydration unchanged — only `loadMore` pages are client-only.
* **`loadMore` is O(page-size)** and writes only the new page's entities (concatenating their ids via
  `setRaw`); it's guarded against overlapping calls and is a no-op once `hasMore` is `false`.
* **`getCursor` reads `string[]` ids.** Offset (`ids.length`) and page-number (`pageIndex`) cursors
  are direct; a keyset cursor over an entity field isn't available here (ids only) — use `useStateData`.
* **The query is keyed by the `params` value.** An identity-unstable but value-equal `params` is
  fine — it won't refetch. Changing the params **value** (or calling `reload()`) resets pagination
  and refetches page 0. Memoizing `params` is still good practice to keep `loadMore` stable.
