skip to content

In an NgRx SignalStore that uses withEntities for products keyed by sku, how do addEntity, setEntity and upsertEntity differ, and where does selectId go?

level: middleimportance: should knowfreq 27%

answer

  1. ids, entityMap, computed entities
  2. standalone updaters inside patchState
  3. add keeps, set replaces, upsert merges
  4. selector goes to the updater
  5. entityConfig bundles it once

basics

~20 s

For an existing id, addEntity keeps the stored product, setEntity replaces it and upsertEntity merges the given properties. withEntities takes no id selector; pass selectId to each add, set, upsert and update call, or bundle it with entityConfig.

solid answer

~30 s

`withEntities<Product>()` adds `ids` and `entityMap` state plus a computed `entities` array, and changes go through standalone updaters passed to `patchState`. For an id already in the collection, `addEntity` keeps the existing entity and throws nothing, `setEntity` replaces it wholesale, and `upsertEntity` merges only the given properties; all three append when the id is new. With a `sku` key, `withEntities` itself stays the same and the selector goes into the config of every `add*`, `set*`, `upsert*` and `update*` call, while `remove*` calls take ids and need none. `entityConfig({ entity: type<Product>(), selectId: (p) => p.sku })` bundles it once for both.

code

ts · 30 lines
ts
import { patchState, signalStore, type, withMethods } from '@ngrx/signals';
import {
  entityConfig, removeEntity, setAllEntities, updateEntity, upsertEntities, withEntities,
} from '@ngrx/signals/entities';

type Product = { sku: string; name: string; stock: number; description?: string };

const productConfig = entityConfig({
  entity: type<Product>(),
  selectId: (p) => p.sku,
});

export const ProductsStore = signalStore(
  withEntities(productConfig),
  withMethods((store) => ({
    loaded(products: Product[]): void {
      patchState(store, setAllEntities(products, productConfig));
    },
    listRefreshed(products: Product[]): void {
      // list items omit description; upsert keeps the one loaded earlier
      patchState(store, upsertEntities(products, productConfig));
    },
    stockChanged(sku: string, stock: number): void {
      patchState(store, updateEntity({ id: sku, changes: { stock } }, productConfig));
    },
    discontinued(sku: string): void {
      patchState(store, removeEntity(sku));
    },
  }))
);

go deeper

for a junior

Recall that withEntities adds ids, entityMap and entities, and that you change the collection by passing updaters such as addEntity to patchState.

for a middle

Explain add versus set versus upsert for an existing id, which updaters need selectId for a custom key, and why remove calls do not.

for a senior

Pick updaters by payload shape: upsert or update for partial server data, set for full replacements, and entityConfig so a custom key is never forgotten.

for a principal

Decide how entity collections are split across stores, when named or private collections are justified, and how to keep the id strategy consistent across the codebase.

## What withEntities adds `withEntities` comes from the `@ngrx/signals/entities` plugin and adds a normalized entity collection to a SignalStore. `signalStore(withEntities<Product>())` gives the store three members: - `ids` — a **state** slice, the ordered array of entity ids (`EntityId`, a `string` or a `number`); - `entityMap` — a **state** slice, an object from id to entity; - `entities` — a **computed** signal that maps `ids` over `entityMap` to produce the array a template iterates. By default an entity must have an `id` property. Lookup by id is a property read on `entityMap()`, and the list keeps its order through `ids`. ## Entity updaters The plugin does not add methods to the store. It ships **standalone updaters** — functions that return a partial-state updater — which you pass to `patchState`: | Updater | Existing id | Missing id | |---|---|---| | `addEntity` / `addEntities` | left unchanged, no error | appended | | `prependEntity` / `prependEntities` | left unchanged, no error | inserted at the start | | `setEntity` / `setEntities` | **replaced** by the new object | appended | | `upsertEntity` / `upsertEntities` | **merged**: given properties overwrite, others stay | appended | | `updateEntity` / `updateEntities` / `updateAllEntities` | partial `changes` applied (object or function) | no-op, no error | | `setAllEntities` | whole collection replaced | — | | `removeEntity` / `removeEntities` / `removeAllEntities` | removed (by id or predicate) | no-op, no error | Because they are plain functions, several updaters combine in one call — `patchState(store, setAllEntities(products), { loading: false })` — and the store changes once. When an updater changes nothing, such as `addEntity` for an id that already exists, it contributes an empty partial state, so no signal is set. ## Products keyed by sku: where selectId goes A product catalogue often identifies items by `sku`, not `id`. `withEntities` itself takes **no** id selector; it only creates the slices. The selector goes to the updaters: 1. Declare `const selectId: SelectEntityId<Product> = (p) => p.sku;`. 2. Pass it in the config of every `add*`, `prepend*`, `set*`, `upsert*` and `update*` call: `addEntities(products, { selectId })`. 3. Omit it on `remove*` calls — they take ids (or a predicate), so no selector is needed. Forgetting it is usually caught at compile time: the updater overloads without a config require the entity type to have an `id: EntityId` property, which `Product` does not. `entityConfig({ entity: type<Product>(), selectId: (p) => p.sku })` bundles the entity type, the selector and an optional collection name into one constant you pass both to `withEntities` and to each updater, so the selector cannot be forgotten. ## Named and private collections `withEntities({ entity: type<Product>(), collection: 'product' })` prefixes the members: `productIds`, `productEntityMap` and `productEntities`. Every updater then needs `{ collection: 'product' }` too. Named collections let one store hold several collections, although the docs recommend a dedicated store per entity type in most cases. A collection name starting with `_` (for example `'_product'`) makes the members **private** to the store, which you then expose selectively through `withComputed`. ## Reading the collection - **Lookup by id** is a property read: `store.entityMap()['A-100']`. It is the natural base for a selected-product computed built from a `selectedSku` state slice. - **Derived lists** are ordinary computeds over `entities()`, such as `lowStock: computed(() => store.entities().filter((p) => p.stock < 5))`. - **Order** comes from `ids`, so `setAllEntities` keeps the server's order, `addEntity` appends, and `prependEntity` puts a fresh item first, for example a newly created product at the top of an admin list. - **Unchanged slices stay untouched.** Each updater returns only the slices it actually changed (`entityMap`, `ids` or both) and an empty partial state when nothing changed, so `patchState` sets no signal and nothing that reads the collection recomputes. ## What interviewers listen for - The difference between **set** (replace) and **upsert** (merge) for an existing id — the most common mix-up, because it decides whether properties missing from a partial server payload survive. - That **add** never overwrites and never throws, so "adding" a fresher copy of a product silently keeps the stale one. - That `entities` is **computed**, so you never patch it directly; you patch `ids` and `entityMap` through updaters. - That custom ids are handled per updater call, or once through `entityConfig`.

  • What does withEntities({ entity: type<Product>(), collection: 'product' }) change about the store and its updaters?
    The members get a prefix: `productIds`, `productEntityMap` and `productEntities` instead of `ids`, `entityMap` and `entities`. Every updater for that collection must then receive `{ collection: 'product' }`, or the bundled `entityConfig`. Named collections let one store hold several entity types, although the docs recommend a dedicated store per type in most cases.
  • Why can a server's partial product payload lose fields when you apply it with setEntity?
    `setEntity` replaces the stored object with the one you pass, so any property the payload omits is gone afterwards. `upsertEntity` merges instead: properties present in the payload overwrite, properties absent from it stay. For partial updates by id, `updateEntity({ id, changes })` is the other option, and it does nothing when the id is missing.

saying these in an interview costs you the question

  • addEntity overwrites the stored entity when the id already exists
  • setEntity and upsertEntity both merge properties into the existing entity
  • The custom selectId is configured once on withEntities and applies everywhere
  • The entities array is state you patch directly
  • removeEntity needs the selectId config to find a custom key