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

# useModelStore \[Subscribe to one normalized entity by id]

Returns the `ModelStore` for a model descriptor, so a component can subscribe to a single
normalized entity without re-fetching the full list.

```tsx
import { asKey } from "rxfy";
import { useAtom, useModelStore } from "rxfy-react";
import { TodoModel } from "./models";

function TodoItem({ id }: { id: string }) {
  const store = useModelStore(TodoModel);
  // `id` is a plain string prop here, so brand it with `asKey`; ids read straight from `data$`
  // are already `StoreKey`s and pass to `get` without it.
  const [todo] = useAtom(store.get(asKey(TodoModel, id)));
  return <li>{todo.title}</li>;
}
```

```ts
function useModelStore<T>(descriptor: ModelDescriptor<T>): ModelStore<T>;
```

The returned `ModelStore<T>` exposes:

```ts
type ModelStore<T> = {
  get<TView extends T = T>(key: StoreKey<TView>): IAtom<TView>; // writable handle; the key's brand picks the view; throws if not loaded
  set(key: string, val: T): void; // write one entity
  setMany(items: T[]): void; // write many; key is derived via getKey
  getValue(key: string): T | undefined; // synchronous read, no subscription, never throws
  observe(key: string): Observable<T | undefined>; // non-throwing reactive read; undefined until present
  valueEntries(): [string, T][]; // snapshot of all loaded [key, value] pairs
  added$: Observable<string>; // a key, the first time its entity appears
};
```

`get(key)` takes a `StoreKey<T>` — a branded id the framework mints. Every id from a fulfilled
state (`data$`/query shape) already is one, so passing those in holds by construction; brand a
raw string with [`asKey`](/rxfy/create-model#storekey--get-only-accepts-framework-minted-ids).
`get` returns the entity's cell itself, so the handle is stable across calls (no `useMemo`
needed). See [Model](/rxfy/create-model) for the full reference on each method.

## View-typed reads

`get` preserves the key's brand. An id from a plain state resolves to the base entity, but an id from
a [joined state](/rxfy/define-state#relations--per-state-joins) is branded with the **view** it was
fetched as — so `get` hands back an entity whose joined relations are *required*, and you read them
without a `!`:

```tsx
import type { NormalizedOf } from "rxfy";

function Article({ id }: { id: NormalizedOf<typeof postState>["post"] }) {
  const [post] = useAtom(useModelStore(PostModel).get(id)); // `id` carries the joined view
  const [category] = useAtom(useModelStore(CategoryModel).get(post.category)); // required — no `!`, no fallback
  return <CategoryBadge id={post.category} />; // thread the branded ref down, not a plain string
}
```

Passing the branded id down (rather than a plain `string`) means the child's `get` returns the same
view — no `asKey`, no re-widening. To name that prop type, index the query shape or deref a key with
[`ViewOf`](/rxfy/define-state#inferring-a-states-types):

```ts
type PostRef = NormalizedOf<typeof postState>["post"];
type CategoryRef = ViewOf<PostRef>["category"];
```

This is the type-safe alternative to `useModelStoreValue` below: reach for a branded ref + `get` when
the state *guarantees* the relation is loaded (SSR-safe, synchronous); reach for `useModelStoreValue`
when it might not be.

## useModelStoreValue

A **non-throwing** reactive read of one entity by id. Returns `T | undefined` — `undefined` while
the id is `null` or the entity hasn't loaded yet, then the entity once it's present. Use it for a
component that may render whether or not a [relation](/rxfy/define-state#relations--per-state-joins)
was joined, where `get` (which throws on an unloaded id) would be too strict:

```tsx
import { useModelStoreValue } from "rxfy-react";
import { CategoryModel } from "./models";

function CategoryBadge({ id }: { id: StoreKey<Category> | null }) {
  const category = useModelStoreValue(CategoryModel, id);
  return <span>{category ? category.name : "—"}</span>;
}
```

```ts
function useModelStoreValue<M>(model: M, id: StoreKey<EntityOf<M>> | null | undefined): EntityOf<M> | undefined;
```

Reach for `get` when the id is guaranteed loaded (the page fetched it); reach for
`useModelStoreValue` when it might not be.
