State Normalization
Flattening nested data into normalized state to kill duplication bugs.
Most real UIs are relational: posts have authors, comments have authors, the same user shows up all over the screen. When the server hands you that as one big nested blob, the same entity gets copied into a dozen places — and keeping those copies in sync becomes a quiet source of bugs. Normalization is the fix.
Relational data, nested JSON
Take a feed. Users see posts, posts have comments, and those comments are written by users. Almost every screen is this web of related entities — tap a node to trace it:
The server usually returns that web as one nested tree — and the same user object gets embedded wherever they appear:
[
{ id: "p1", author: { id: "u1", name: "Hemdan" },
comments: [
{ id: "c1", author: { id: "u3", name: "Sara" } },
{ id: "c2", author: { id: "u1", name: "Hemdan" } } // u1 again
]
},
{ id: "p2", author: { id: "u2", name: "Layla" },
comments: [{ id: "c3", author: { id: "u1", name: "Hemdan" } }] } // and again
]Drawn as a tree, the shape of one post makes the problem obvious — the author hangs off the post and off every comment, so the same user is copied again and again:
User u1 appears three times, each a separate copy. There's no single source of truth for “who is u1.”
Why nested state hurts
Four problems fall out of that duplication. Tap each:
Rename a user — watch it break
Here's the bug live. In Nested mode, renaming the user does the easy update (post authors) but misses the copies buried in comments. Switch to Normalized and the same rename touches one entity — everything updates at once. Flip to Autoplay to watch:
id, store entities once in lookup tables, and reference them by id everywhere else. One update to the table propagates to every reference — a true single source of truth.The normalization process
Think of your state like database tables. Flattening the tree comes down to three rules — they map straight onto the nested response:
const feed = [ { id: "post1",← Stable ID author: { id: "u1", name: "Hemdan" },← Relation body: "Hello", comments: [← Ordered list { id: "c1", author: { id: "u1" } } ] }]Apply them and you get one lookup table per entity, with an allIds array preserving order:
{
users: { byId: { u1: { name: "Hemdan" }, u2: {}, u3: {} } },
posts: { byId: { p1: { authorId: "u1", commentIds: ["c1","c2"] } },
allIds: ["p1", "p2"] }, // order preserved
comments: { byId: { c1: { authorId: "u3" }, c2: { authorId: "u1" } } }
}It's the same idea as relational database tables: each entity lives in its own table, and foreign keys (ids) wire them together instead of copying data around.
normalize()transform on arrival (and denormalize() if older components still expect the nested shape).The trade-offs
| You gain | You pay | |
|---|---|---|
| Updates | Change once, propagates everywhere | More code (normalize / denormalize helpers) |
| Consistency | Single source of truth | Selectors to re-assemble nested views |
| Rendering | Stable references, fewer re-renders | Mental overhead for the whole team |
| Best when | Entities repeat + update often | Overkill for small, static trees |
The four costs in detail — tap each:
When to normalize (and when to skip)
Sort each scenario — is normalization worth it here?
Check your understanding
Design challenge
Take a Trello-style board (boards → lists → cards, with members):
- Write the nested shape, then the normalized tables (
byId+allIds). - Show how renaming a member updates everywhere with one write.
- Decide honestly: is this app update-heavy enough to justify it?
- →Most UI data is relational; nested JSON duplicates entities and loses a single source of truth.
- →Nested state causes duplicate truth, hard deep updates, extra re-renders, and inconsistency.
- →Normalize by storing entities by id, referencing by id, and keeping order in
allIds. - →Do it as a client-boundary transform — normalize on fetch, denormalize for views that need it.
- →Worth it when entities repeat and update often; skip it for small, static, server-owned data.