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

# Create Store \[Normalized reactive state in a client-only app]

The store path is rxfy with no server: typed models, normalized entities, and reactive
subscriptions in a plain React app. The idea in one paragraph: rxfy stores each entity
**once**, in a shared store keyed by its id. A query holds only ids and resolves entities
from the store, so when you write an entity — from a refetch, a mutation, or a websocket
push — every component that is subscribed to this entity updates automatically.

## Install

Install the two rxfy packages plus the `rxjs`/`zod`/`lodash` peers:

:::code-group

```bash [npm]
npm install rxfy rxfy-react
npm install rxjs zod lodash
```

```bash [pnpm]
pnpm add rxfy rxfy-react
pnpm add rxjs zod lodash
```

```bash [yarn]
yarn add rxfy rxfy-react
yarn add rxjs zod lodash
```

```bash [bun]
bun add rxfy rxfy-react
bun add rxjs zod lodash
```

:::

rxfy declares `rxjs ^7`, `zod ^4`, and `lodash ^4` as **peer dependencies** so your whole
app shares one copy of each; npm does not install them for you, hence the second line.
`rxfy-react` also needs `react`/`react-dom` 18+, which your React app already has.

## Wrap your app

`StoreProvider` creates the model registry that `useStateData` and `useModelStore`
write to and read from. Wrap your root once:

```tsx [main.tsx]
import { StoreProvider } from "rxfy-react";
import { createRoot } from "react-dom/client";
import { App } from "./App";

createRoot(document.getElementById("root")!).render(
  <StoreProvider>
    <App />
  </StoreProvider>,
);
```

A client-only app passes no props. The SSR props (`ssr`, `registry`, `dehydratedState`)
are covered in [Server-Side Rendering](/core-concepts/ssr).

## Fetch, render, update

One file: a model, a state, a fetch, and two components.

```tsx [App.tsx]
import { z } from "zod";
import { array, asKey, createModel, defineState } from "rxfy";
import { Pending, useAtom, useModelStore, useStateData } from "rxfy-react";

// A model: an entity type plus how to read its id.
const UserModel = createModel({
  schema: z.object({ id: z.string(), name: z.string() }),
  getKey: (user) => user.id,
  name: "user",
});

// A state: what one query fetches — here, a list of users.
const usersState = defineState({
  key: "users",
  params: z.object({}),
  model: { users: array(UserModel) },
});

// Stands in for a real API call; the signature stays the same when you swap it.
async function fetchUsers() {
  return {
    users: [
      { id: "1", name: "Ada Lovelace" },
      { id: "2", name: "Grace Hopper" },
    ],
  };
}

// Each row subscribes to its own entity by id and re-renders only when that entity changes.
function UserRow({ id }: { id: string }) {
  const store = useModelStore(UserModel);
  // `id` here is a plain string prop, so brand it with `asKey`. (Type the prop as
  // `StoreKey<User>` instead and the query-shape id flows in with no `asKey`.)
  const [user] = useAtom(store.get(asKey(UserModel, id)));
  return <li>{user.name}</li>;
}

export function App() {
  const { data$ } = useStateData({ state: usersState, fetchFn: fetchUsers, params: {} });
  const store = useModelStore(UserModel);

  return (
    <>
      <Pending value$={data$} pending={<p>Loading…</p>}>
        {({ users }) => (
          <ul>
            {users.map((id) => (
              <UserRow key={id} id={id} />
            ))}
          </ul>
        )}
      </Pending>
      <button onClick={() => store.set("1", { id: "1", name: "Ada King" })}>Rename Ada</button>
    </>
  );
}
```

Run it. You see two names; click **Rename Ada** and the first row changes.

What happened, in order:

1. `useStateData` ran `fetchUsers` and **normalizes** the result: each user was written
   into the shared `UserModel` store under its id, and `data$` emitted the query shape —
   `{ users: ["1", "2"] }`. It holds ids, not user objects so you can then reference this entity right from the store by its id.
2. Each `UserRow` resolved its own entity with `store.get(id)` — a synchronous, writable
   handle. Its id came out of the fulfilled query, so the entity is already in the store;
   there is nothing to wait for.
3. The button called `store.set` with a new entity for id `"1"`. The row subscribed to
   that id re-rendered; the list and the other row did not, and nothing re-fetched.

Step 3 is the point of the library. A refetch, a mutation, and a websocket message all
land the same way — one write to the store, and every view of that entity updates with no extra (declarative) friction.

Notice what unwraps where. The button sits **outside** `Pending`: a write needs no
unwrapped data, so it never waits on the query. `App` unwraps only the id list; each
`UserRow` reads its one entity synchronously. rxfy keeps values wrapped until the leaf
that renders them — unwrap late, and both loading UI and re-renders stay exactly where
the data is used.

:::tip
`params` compares by value, so passing a fresh `{}` each render is fine. What you fetch is
declared by the state's `params` schema; give it real fields and `useStateData` re-fetches
whenever their values change.
:::

## Writes as mutations

`store.set` is the low-level write. For app-level changes you usually declare **mutations** on
the state instead: named reducers that take the whole fetched shape and return the next one.
rxfy re-normalizes the result back into the store, so every subscribed view updates exactly the
way the `store.set` button did — you describe the change, not the bookkeeping.

Give `usersState` a `mutations` block:

```ts [App.tsx]
const usersState = defineState({
  key: "users",
  params: z.object({}),
  model: { users: array(UserModel) },
  mutations: {
    addUser: (prev, name: string) => ({
      users: [...prev.users, { id: crypto.randomUUID(), name }],
    }),
    rename: (prev, next: User) => ({
      users: prev.users.map((u) => (u.id === next.id ? next : u)),
    }),
  },
});

type User = z.infer<typeof UserModel.schema>;
```

`useStateData` returns those mutations alongside `data$`; call them from anywhere in the tree:

```tsx [App.tsx]
const { data$, mutations } = useStateData({ state: usersState, fetchFn: fetchUsers, params: {} });

// ...somewhere in the render:
<button onClick={() => mutations.addUser("Katherine Johnson")}>Add user</button>;
```

Reducers work with whole entities, not ids; normalizing the result back into the store is rxfy's
job. A mutation and a websocket push land identically — one write, and every view of that entity
re-renders with no re-fetch.

## Local drafts with Atom and Lens

A form wants a draft the user can type into — and discard — without touching the shared store
until they commit. Hold that draft in a local [`Atom`](/rxfy/create-atom), focus the one field
you're editing with a [`Lens`](/rxfy/create-lens), and bind it with
[`useAtom`](/react/use-atom). The form never reads or writes the store; on submit it hands the
edited entity to `onSave`, and the parent runs the write:

```tsx [EditName.tsx]
import { useMemo } from "react";
import { createAtom, createLens, keyLens } from "rxfy";
import { useAtom } from "rxfy-react";

type User = { id: string; name: string };

export function EditName({ user, onSave }: { user: User; onSave: (user: User) => void }) {
  const draft$ = useMemo(() => createAtom(user), [user]);
  const name$ = useMemo(() => createLens(draft$, keyLens("name")), [draft$]);
  const [name, setName] = useAtom(name$);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSave(draft$.get());
      }}
    >
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button type="submit">Save</button>
    </form>
  );
}
```

`draft$` is a plain `Atom<User>` local to this component; `name$` is a `Lens` onto its `name`
field, and `useAtom` binds that field to the input. Typing updates the draft, not the store; the
row keeps showing the saved name until you submit. Wire `onSave` to the `rename` mutation and the
edit reaches every view of that user at once. `onSave` is your write boundary — against a real
backend it's where you'd `await` a `PATCH` before (or after) the store write.

## Next: Add SSR

Your store works on the client. The next level is
[Add SSR](/getting-started/add-ssr) — render the first paint on the server and hydrate it
with no refetch. Same models, same components; no server push yet.
