Entity State Normalization
Entity state stores collections as id-keyed maps for constant-time lookups, rather than arrays that require linear search.
Entity State Interface
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
export interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
export interface UserState extends EntityState<User> {
loading: boolean;
error: string | null;
}
Creating an Entity Adapter
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
selectId: (user) => user.id, // Default: 'id' property
sortComparer: false, // Default: no sorting
});
Initial State
export const initialState: UserState = userAdapter.getInitialState({
loading: false,
error: null,
});
Adapter State Shape
{
ids: [1, 2, 3], // Array of IDs (order preserved)
entities: { // Map of id -> entity
1: { id: 1, name: 'Alice', email: 'alice@example.com', isActive: true },
2: { id: 2, name: 'Bob', email: 'bob@example.com', isActive: false },
3: { id: 3, name: 'Charlie', email: 'charlie@example.com', isActive: true },
},
loading: false,
error: null,
}
Adapter Operations and Selectors
EntityAdapter provides methods for common collection operations and generates selectors.
Adapter Methods
// Reducer with adapter operations
export const userReducer = createReducer(
initialState,
on(UserActions.loadUsersSuccess, (state, { users }) =>
userAdapter.setAll(users, { ...state, loading: false })
),
on(UserActions.addUser, (state, { user }) =>
userAdapter.addOne(user, state)
),
on(UserActions.updateUser, (state, { user }) =>
userAdapter.upsertOne(user, state)
),
on(UserActions.deleteUser, (state, { id }) =>
userAdapter.removeOne(id, state)
),
on(UserActions.deleteUsers, (state, { ids }) =>
userAdapter.removeMany(ids, state)
),
);
Adapter Method Reference
| Method | Purpose |
|---|---|
setAll(items, state) |
Replace entire collection |
addOne(item, state) |
Add single item |
addMany(items, state) |
Add multiple items |
upsertOne(item, state) |
Add or update single item |
upsertMany(items, state) |
Add or update multiple items |
updateOne({ id, changes }, state) |
Update single item |
updateMany(updates, state) |
Update multiple items |
removeOne(id, state) |
Remove single item |
removeMany(ids, state) |
Remove multiple items |
removeAll(state) |
Clear collection |
Adapter-Generated Selectors
export const {
selectIds, // (state) => state.ids
selectEntities, // (state) => state.entities
selectAll, // (state) => User[]
selectTotal, // (state) => number
} = userAdapter.getSelectors(selectUserState);
// In component
users$ = this.store.select(selectAll);
userCount$ = this.store.select(selectTotal);
Why Normalize?
| Operation | Array | Entity (Map) |
|---|---|---|
| Find by ID | O(n) | O(1) |
| Update item | O(n) | O(1) |
| Insert item | O(1) | O(1) |
| Delete item | O(n) | O(1) |
| Order preservation | Yes | Via ids array |
Quiz
1. What does EntityAdapter store?
2. What is the benefit of normalized entity state?
3. Which adapter method adds or updates an item?
Flashcards
Question
What does EntityAdapter store?
Click to reveal answer
Answer
{ ids: [], entities: { id: entity } } — id-keyed map for O(1) lookups.
Question
Why normalize state?
Click to reveal answer
Answer
Constant-time lookups by ID. O(1) find/update/delete vs O(n) with arrays.
Question
Which method adds or updates?
Click to reveal answer
Answer
upsertOne — adds if new, updates if exists.
Revision Notes
Key Takeaways
- 1. EntityAdapter normalizes collections into id-keyed maps
- 2. O(1) lookups vs O(n) with arrays
- 3. upsertOne adds or updates
- 4. Adapter generates selectors (selectAll, selectTotal)
Interview Tips
- • Explain the normalized state shape
- • Be ready to describe performance benefits
- • Mention adapter methods for common operations
Cheat Sheet
Entity State Cheat Sheet
Create adapter:
const adapter = createEntityAdapter<User>({
selectId: (user) => user.id,
});
State shape:
{ ids: [1, 2], entities: { 1: {...}, 2: {...} } }
Operations:
- setAll: replace collection
- addOne: add single
- upsertOne: add or update
- removeOne: remove by ID
Selectors:
const { selectAll, selectTotal } = adapter.getSelectors(selectUserState);