Prompt
APIs return arrays; stores want lookup tables. Write entitiesReducer(state, action) over { byId: {}, allIds: [] }:
{ type: 'ADD', entity }— entity has anid; adds to both structures (ignore if id exists){ type: 'UPDATE', id, changes }— shallow-merges changes into the entity (no-op if missing){ type: 'REMOVE', id }— removes from both structures- Pure — new objects on every change,
allIdspreserves insertion order
Solution
Normalization is the answer to a question juniors don't know they have: "why is updating one item in my list so awkward?" With an array, an update is a map scan, a lookup is a find scan, and the same user appearing in two lists means two copies that drift apart. byId + allIds makes updates O(1), keeps exactly one copy of each entity, and preserves order separately from identity — it's a relational table, which is why Redux Toolkit ships it as createEntityAdapter.
The moves that read as senior:
- UPDATE spreads three levels — state, byId, entity — because immutability isn't "use spread once"; every object on the path to the change must be new, and everything off the path must keep its reference. That reference stability is what makes
memo'd list rows skip re-rendering. - REMOVE uses destructuring-with-rest (
const { [id]: removed, ...rest }) to drop a dynamic key without mutating — the idiomatic alternative todeleteon a copy. - Guards return the same reference for no-ops (add-existing, update-missing) so subscribers don't wake up for nothing.
In React Native this pattern has a second payoff: FlatList wants data (order) and fast row lookup separately — allIds is your data prop, byId is your row resolver, and an update to one row changes one reference instead of remapping the whole array.
Red flag: storing the array and "normalizing later." The migration never happens; every new feature adds another .find(). Normalize at the reducer boundary — the API shape is the server's concern, the store shape is yours.
Say it: "I normalize at the reducer: byId for O(1) identity, allIds for order, spread every object on the update path — same model as createEntityAdapter, and it's what keeps memoized list rows stable."