Entity adapters: Managing nested relationships and cascading deletes
0 reputation · 08 Aug 2020, 13:11 UTC
0 reputation · 08 Aug 2020, 13:11 UTC
To keep a normalized state that reflects a parent-child relationship between two entity collections while ensuring that deleting a parent automatically removes its children from the store.
The @ngrx/entity library offers adapters that generate CRUD reducers and selectors, but it does not provide a built‑in mechanism for nested or parent‑child relationships. A sortComparer that returns 0 for equal values can lead to unstable ordering across updates, which complicates maintaining consistent relationships.
Given these constraints, the team needs to decide how to implement cascading deletes and maintain referential integrity without breaking the immutable update guarantees of the store.
34775 reputation · 08 Aug 2020, 15:44 UTC
Model the parent-child link with a foreign key on the child (for example, child.parentId), then implement cascading deletes in a custom reducer that removes matching children and the parent in a single immutable update. There is no built-in ngrx/entity mechanism for nested relationships, and there is no officially endorsed linking pattern beyond explicit foreign-key IDs.
The adapter generates collection-level CRUD reducers only, so cascading behavior requires a helper selector and a custom delete case. Stability is restored by making the comparer a strict weak ordering: when the primary field ties, fall back to a unique immutable field such as entity.id.
const selectChildrenByParentId = createSelector(
selectAllChildren,
(children, parentId) => children.filter((c) => c.parentId === parentId)
);
// In the parent reducer delete case
const childrenToRemove = selectChildrenByParentId(state, action.payload.parentId);
const nextChildIds = state.childIds.filter(
(id) => !childrenToRemove.some((c) => c.id === id)
);
return {
...state,
parentIds: state.parentIds.filter((id) => id !== action.payload.parentId),
childIds: nextChildIds
};
const sortComparer = (a, b) => {
if (a.businessField < b.businessField) return -1;
if (a.businessField > b.businessField) return 1;
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
};
Confirm whether children store a direct parentId or whether the relationship lives in a separate lookup table or is many-to-many. If it is not a direct foreign key, the cascade logic must be redesigned.
Assumptions: entities have a unique id field, updates are synchronous within a single reducer, and the store uses ngrx/entity selectors generated from the adapter.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 08 Aug 2020, 15:17 UTC
NgRx Entity is flat‑state only, so a parent’s removal must be followed by a child filter. Create one action, e.g. deleteParentAndChildren(parentId), and in its reducer first call adapter.removeOne(parentId) on the parent entity adapter, then call a custom adapter.removeMany on the child adapter with a predicate that matches child.parentId === parentId. This keeps the update atomic and preserves immutability.
When sortComparer can return 0 for equal values, add a secondary key such as the entity’s id or a timestamp: (a,b)=>a.value===b.value? a.id-b.id : a.value-b.value. The tie‑breaker guarantees deterministic indices after each update.
Use a selector that combines the two flat collections: createSelector(selectParents, selectChildren, (parents, children)=>parents.map(p=>({…p, items: children.filter(c=>c.parentId===p.id)}))). This keeps the UI logic out of the reducer while still reflecting the relationship.