In an NgRx reducer, a kanban card moves columns via the entity adapter's updateOne; what changes in state, and what if the id is unknown?
answer
- id plus partial changes
- shallow merge into the stored card
- new entities, ids usually kept
- missing id is not an error
- full push: upsertOne or setOne
basics
~20 supdateOne takes { id, changes }, shallow-merges the changes into the stored card and returns state with a new entities map, keeping ids unless the key or sort slot changed. An unknown id returns the same state object.
solid answer
~40 s`cardAdapter.updateOne({ id: 'c42', changes: { columnId: 'done' } }, state)` builds a new card from the stored one plus `changes`, a shallow merge: fields you omit survive and a nested object in `changes` replaces the old one wholesale. It returns a new state with a new `entities` map; `ids` keeps its reference unless the key changed or, with a `sortComparer`, the card's slot moved. If no card has that id, nothing happens and the same state object comes back, with no error, so a move for a card that was never loaded silently does nothing. Its neighbours differ exactly there: `addOne` ignores an existing id, `setOne` replaces the whole card, `upsertOne` inserts a missing card or merges into an existing one, and `mapOne` computes the change from the current card.
code
ts · 22 linesimport { createReducer, on } from '@ngrx/store';
import { BoardActions } from './board.actions';
import { cardAdapter, initialState } from './board.state';
export const boardReducer = createReducer(
initialState,
on(BoardActions.cardMoved, (state, { cardKey, columnId, position }) =>
cardAdapter.updateOne({ id: cardKey, changes: { columnId, position } }, state)
),
on(BoardActions.cardPushed, (state, { card }) =>
cardAdapter.upsertOne(card, state)
),
on(BoardActions.cardVoted, (state, { cardKey }) =>
cardAdapter.mapOne(
{ id: cardKey, map: (card) => ({ ...card, votes: card.votes + 1 }) },
state
)
),
on(BoardActions.cardArchived, (state, { cardKey }) =>
cardAdapter.removeOne(cardKey, state)
)
);go deeper
Know the call shape, updateOne({ id, changes }, state), and that changes is partial.
Explain the shallow merge, the silent no-op on an unknown id, and how addOne, setOne, upsertOne and updateOne differ on existing records.
Use the no-op and reference behaviour when debugging a move that did nothing, and pick upsertOne or setOne for server pushes on purpose.
Agree on whether server payloads are complete or sparse across features, since that choice decides upsertOne versus setOne everywhere.
## The Update<T> shape `updateOne` is the `@ngrx/entity` method for changing part of one record. Its first argument has the **`Update<T>`** shape: - **`id`** — the key of the record, a `string` or `number` matching what `selectId` returns. - **`changes`** — a `Partial<T>`: only the fields that change. In the reducer it looks like this: ```ts on(BoardActions.cardMoved, (state, { cardKey, columnId, position }) => cardAdapter.updateOne({ id: cardKey, changes: { columnId, position } }, state) ) ``` `updateMany` takes an array of the same objects and applies them in one pass. ## What the adapter does For each update, the adapter copies `ids` and `entities`, then: 1. **Skips unknown ids.** Updates whose `id` is not in `entities` are dropped. If none remain, the call returns the **state object you passed in**, not a copy. 2. **Merges shallowly.** The new card is `{ ...stored, ...changes }` in effect. Omitted fields keep their values; a nested object in `changes` replaces the stored nested object instead of merging into it. 3. **Checks the key.** If the merged card's `selectId` value differs from `id`, the card is **re-keyed**: the old key is deleted and the new key takes its place in `ids`. 4. **Places it.** Without a `sortComparer` the card keeps its slot. With one, the card is merged back into sorted order. 5. **Returns the smallest change.** If only `entities` changed, the result is `{ ...state, entities }` with the **original `ids` array**. If a key or position changed, `ids` is new as well. ## Choosing among the write methods The same kanban board uses several write methods, and the difference is what each does when the record already exists or is missing: | Method | Card already stored | Card missing | |---|---|---| | `addOne` | ignored, state unchanged | added | | `setOne` | replaced entirely | added | | `upsertOne` | shallow-merged with the given card | added | | `updateOne` | shallow-merged with `changes` | ignored, state unchanged | | `mapOne` | shallow-merged with `map(card)` | ignored, state unchanged | Typical use on the board: - **Drag between columns:** `updateOne` with the new `columnId` and `position`. - **Server pushes a full card:** `upsertOne` when optional fields you already hold should survive a sparse payload; `setOne` when the payload is the whole truth. - **Initial load of the board:** `setAll`, which replaces the collection. - **Bump a counter from its current value:** `mapOne({ id, map: (card) => ({ ...card, votes: card.votes + 1 }) })`, so the reducer does not need the old value in the action. - **Archive:** `removeOne(cardKey)`; `removeMany` takes keys or a predicate. ## Moving several cards at once Dragging a card into the middle of a column usually shifts its neighbours' positions too. Dispatching one action per card works, but each adapter call copies the collection again and each produces a new state for every selector to see. `updateMany` handles the batch in one call: - Pass an array of `{ id, changes }` objects, one per card whose `position` or `columnId` changed. - Unknown ids in the batch are skipped individually; the rest still apply. - The adapter copies `ids` and `entities` once for the whole batch, and with a `sortComparer` it merges every moved card back into order in the same pass. - Selectors downstream see one new state instead of a burst of intermediate ones. `upsertMany`, `setMany`, `addMany` and `removeMany` follow the same pattern for their single-record counterparts. ## Why the silent no-op matters Because an unknown id is not an error, a bug where the move action carries the wrong key, or arrives before the board has loaded, produces no exception and no state change. The view simply does not move the card. When debugging a "drag did nothing" report, check the action payload's key against `ids` in the DevTools state before suspecting the reducer. ## References and change detection That `updateOne` returns the same `ids` when order is unchanged is useful downstream: - A selector that reads only `ids` returns the same array, so a column list keyed on ids sees no new value. - `selectAll` reruns, because `entities` is new, and returns a new array with the moved card in it. - A no-op update returns the original state, so nothing downstream recomputes at all.
- A card has an optional dueDate; a server push omits it. How do upsertOne and setOne differ?`upsertOne` shallow-merges the pushed card into the stored one, so the stored `dueDate` survives because the push has no such key. `setOne` replaces the stored card with the pushed object, so `dueDate` is gone. Pick `setOne` when the payload is the complete truth and `upsertOne` when it may be sparse.
- When is mapOne a better fit than updateOne?When the new value depends on the current one, such as incrementing votes or toggling a flag. `mapOne({ id, map })` receives the stored card and returns the updated one, so the action carries only the key and the reducer never needs the old value passed in. Like `updateOne`, it does nothing for an unknown id.
saying these in an interview costs you the question
- updateOne on a missing id adds the card with the partial fields.
- updateOne deep-merges nested objects in changes.
- addOne overwrites a card that already exists.
- upsertOne and setOne behave the same for an existing card.
- updateOne throws when the id is not in the collection.