Part III β The Speed Β· Lesson 6.6
Normalization
In one lineStore every entity once, by id, and reference it everywhere else β duplicates are bugs waiting to happen.
Where you've seen itSomeone updates their display name, and it changes on their post but not on their three comments below it.
Diagram
NESTED β the API shape, copied straight into the store
posts: [
{ id:'p1', title:'β¦', author:{ id:'u1', name:'Asha', avatar:'a.png' },
comments:[ { id:'c1', author:{ id:'u1', name:'Asha' } },
{ id:'c2', author:{ id:'u1', name:'Asha' } } ] },
{ id:'p2', author:{ id:'u1', name:'Asha' } }
]
β²β²β²β² four copies of the same user
rename u1 β you must find and update FOUR places
miss one β inconsistent UI, unreproducible bugNORMALIZED β one row per entity, ids as the glue
users: { u1: { id:'u1', name:'Asha', avatar:'a.png' } }
comments: { c1: { id:'c1', authorId:'u1', postId:'p1' },
c2: { id:'c2', authorId:'u1', postId:'p1' } }
posts: { p1: { id:'p1', title:'β¦', authorId:'u1',
commentIds:['c1','c2'] },
p2: { id:'p2', authorId:'u1', commentIds:[] } }
feed: ['p1','p2'] β order lives separately from data
rename u1 β ONE write. Everything re-renders correctly.THE SHAPE β entities + order, kept apart
βββββββββββββββββββββββββββββββ
β byId: { id β entity } β the data
βββββββββββββββββββββββββββββββ€
β allIds/feed: [id, id, id] β the order, per view
βββββββββββββββββββββββββββββββ
lets one entity appear in many lists without copying itHow it works
- Flatten by entity type on the way in β one dictionary per type, keyed by id.
- Replace nested objects with id references, and keep list order as a separate array of ids.
- Selectors re-join the pieces at render time (
selectPostWithAuthor), memoised so the join isn't repeated. - Normalise once, at the API boundary β a small mapper per endpoint, not scattered through components.
- Tools do this for you:
normalizr, RTK Query'sentityAdapter, Apollo's cache, and Relay all implement the same idea.
Trade-offs
| Normalize when | Skip it when |
|---|---|
| The same entity appears in several views | Data is read-only and short-lived |
| Entities are updated in place | The response maps 1:1 to one screen |
| Real-time events patch single records | You're rendering a static report |
| Lists are long and partially shared | The app is small and flat |
Cost, stated honestly: an extra transform layer, selectors to write, and a shape that no longer mirrors the API. For a five-screen app that's overhead; for a feed with live updates it's the difference between correct and haunted.
Interview angle
"A WebSocket sends you an updated post. How does the UI update?"
- Normalized store: write the entity once by id, and every list referencing it re-renders consistently.
- Denormalized store: you'd have to locate every copy, and the one you miss becomes an intermittent bug.
- Keep order arrays separate from entities so live inserts and removals don't disturb the data itself.
Recap
- One entity, one place, referenced by id.
- Keep order in separate id arrays so a record can appear in many lists.
- Normalize at the API boundary; re-join with memoised selectors.