<!--
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
-->

# useStateData \[Fetch, normalize, and subscribe to a query]

Fetches data, normalizes entities into model stores, and returns a `StateHandle`.
**`data$` emits the normalized query shape: entity ids, not entities** (`array` fields
→ `string[]`, `single` fields → `string`). Render lists by id and read entity data
through [`useModelStore`](/react/use-model-store).

```tsx
import { useMemo, useState } from "react";
import { useStateData, useModelStore, Pending } from "rxfy-react";
import { todosState, fetchTodos, TodoModel } from "./todos";

function TodoApp() {
  const [filter, setFilter] = useState<"all" | "active" | "done">("all");
  const params = useMemo(() => ({ filter }), [filter]);

  const { data$, mutations, reload } = useStateData({ state: todosState, fetchFn: fetchTodos, params });

  return (
    <Pending value$={data$} pending={<p>Loading...</p>}>
      {({ todos }) => (
        <>
          <ul>
            {todos.map((id) => (
              <TodoItem key={id} id={id} />
            ))}
          </ul>
          <button onClick={() => mutations.addTodo({ id: crypto.randomUUID(), title: "new", done: false })}>Add</button>
          <button onClick={reload}>Reload</button>
        </>
      )}
    </Pending>
  );
}
```

```ts
function useStateData<TParams, TShape, TMutations>(config: {
  state: StateDescriptor<TParams, TShape, TMutations>;
  fetchFn: (params: TParams, signal: AbortSignal) => Promise<TShape>;
  params: TParams;
  defaultData?: TShape;
}): StateHandle<TShape, TMutations>;
```

`data$` emits the normalized query shape — entity ids for `array`/`single` fields, and
[plain value fields](/rxfy/define-state#plain-value-fields) (bare zod schemas in the `model`)
passed through with their real value.

The returned `StateHandle<TShape, TMutations>` exposes:

```ts
type StateHandle<TShape, TMutations> = {
  data$: Observable<QueryShapeOf<TShape>>; // ids only; stable identity across renders & reloads
  set(value: Updater<TShape>): void; // full entities (normalize/denormalize)
  setRaw(ids: Updater<WritableQueryShapeOf<TShape>>): void; // id shape and/or entities; objects are normalized
  reload(): void;
  mutations: BoundMutations<TShape, TMutations>; // full entities
};

type Updater<T> = T | ((prev: T) => T);
```

See [`set` vs `setRaw`](#set-vs-setraw--writing-entities-vs-writing-the-id-list) below for the
full reference on writing entities.

**Caching**: results, mutations, and `set` write through to the registry's query cache, so a
remount with the same params starts from cached ids without re-fetching, while entity values
always come live from model stores. `reload()` re-fetches **in place** — it flips the shared
query atom to pending and fetches into it, so every component reading the same state updates
together and `data$` keeps its identity (the current data stays visible until the refetch
settles).

`data$` keeps a **stable identity** across re-renders, across a changing `defaultData`, and
across a `params` object whose value is unchanged (the query is keyed by the params value, not
its reference) — so consumers like [`Pending`](/react/pending) don't reset. `set` and `setRaw`
cancel any in-flight fetch before committing, so an explicit write is never clobbered by a late
result.

## `defaultData` — seeding state from a framework loader

Pass `defaultData` to pre-populate the state from data already fetched by the framework
(e.g. a react-router `loader`). The data is normalized into model stores immediately and
`fetchFn` is **not called** on first render. If the cache entry is already populated
(e.g. a remount), `defaultData` is ignored.

```tsx
import { useLoaderData } from "react-router";
import { useStateData, Pending } from "rxfy-react";
import { todosState, fetchTodos } from "./todos";

export async function loader() {
  return fetchTodos({ filter: "all" }, new AbortController().signal);
}

export default function TodoPage() {
  const loaderData = useLoaderData();
  const params = { filter: "all" };

  const { data$, mutations } = useStateData({
    state: todosState,
    fetchFn: fetchTodos,
    params,
    defaultData: loaderData,
  });

  return (
    <Pending value$={data$}>
      {({ todos }) => (
        <ul>
          {todos.map((id) => (
            <TodoItem key={id} id={id} />
          ))}
        </ul>
      )}
    </Pending>
  );
}
```

:::note
When `params` changes (navigation to a new route), the new cache key starts `IDLE` and
`defaultData` is applied again — so the next loader's data seeds the next page without
a redundant fetch.
:::

`defaultData` is the right fit when the framework already owns data fetching and you want to
hand rxfy a head start. The alternative is to let **rxfy own SSR end to end**: components fetch
on the server (Suspense), the per-request registry is dehydrated into the HTML, and the client
hydrates it — so you never pass `defaultData` at all, and the framework loader stays a
**routing-only** concern (URL validation, redirects, param extraction). The
[rr7-blog example](/examples#rr7-blog) takes exactly this approach on React Router 7: its
loaders never fetch domain data. See the [SSR guide](/core-concepts/ssr) for wiring it up.

## `set` vs `setRaw` — writing entities vs writing the id list

`set` is the high-level writer: you hand it the **denormalized shape** (full entities) and it
does the round-trip — splits entities into the model stores and the ids back into `data$`. The
updater form (`set(prev => …)`) first **denormalizes** the current ids into entities so your
reducer sees full objects. That round-trip is O(list length): it rebuilds and re-writes every
entity in the query, even ones that didn't change.

`setRaw` is the low-level sibling: you write the **normalized id shape** directly (the same shape
`data$` emits), with no denormalize round-trip on the current list.

```ts
set:    (value: Updater<TShape>) => void;                       // full entities
setRaw: (ids:   Updater<WritableQueryShapeOf<TShape>>) => void; // ids and/or entities
// Updater<T> = T | ((prev: T) => T)  — note: setRaw's updater receives prev as ids (QueryShapeOf)
```

`setRaw` accepts the **normalized id shape**, but each model-field slot may also hold full entity
objects (or a mix): object elements are written to their model stores for you, strings pass through
as ids. The updater form receives the current **ids** (no denormalize round-trip), so appending a
page stays O(page size) — only the new entities are written. Use `setRaw` whenever re-normalizing the
whole list with `set` would be wasteful: appending a page, prepending, reordering, de-duplicating, or
optimistically removing a row.

```tsx
import { feedState } from "./feed";

function useAppendPage() {
  const { setRaw } = useStateData({ state: feedState, fetchFn: fetchFirst, params });

  // pass new entities by object — setRaw writes them to the store and appends their ids
  return (page: { items: FeedItem[] }) => setRaw((prev) => ({ items: [...prev.items, ...page.items] }));
}
```

The updater form reads the current ids and is a **no-op until the query is `FULFILLED`** (there's
no list to amend yet). For the common pagination case you don't call `setRaw` yourself —
[`useStatePagedData`](/react/use-state-paged-data) composes it for you.

:::info
Passing entities to `setRaw` costs O(objects passed) — only the objects are normalized, ids are
free. Passing the **entire** list as objects is equivalent to `set`; reach for that instead when
you genuinely intend to rewrite every row.
:::
