Frontend System Design/State Normalization
Lesson 15 of 17 · Episode 15

State Normalization

Flattening nested data into normalized state to kill duplication bugs.

NormalizationEntitiesCaching
Watch on YouTube ↗

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.

The shape of data

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:

Users → Posts → Comments → Users. The same Users entity sits at both ends.

The server usually returns that web as one nested tree — and the same user object gets embedded wherever they appear:

nested feed — the author is duplicated
[
  { 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:

“Author” shows up twice — the nested response copies the user object.

User u1 appears three times, each a separate copy. There's no single source of truth for “who is u1.”

The pain

Why nested state hurts

Four problems fall out of that duplication. Tap each:

See it break, then fix

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:

What the user sees
Hemdan
Shipped the new dashboard 🚀
Sara· comment
Looks clean!
Hemdan· comment
Thanks — more soon.
Layla
Why we moved to cursors
Hemdan· comment
Great write-up 👏
The core idea
Give every entity a stable 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.
How to do it

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" } }
]
}
]
Stable IDs
Every entity gets a unique, unchanging id.
Relations as ids
Reference other entities by id — not embedded copies.
Ordered lists as id arrays
Keep order in allIds: ["post1", "post2"].

Apply them and you get one lookup table per entity, with an allIds array preserving order:

normalized — one table per entity
{
  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.

Users
username
name
Posts
id
author_usernameUsers
content
Comments
id
author_usernameUsers
post_idPosts
content
Pink markers are foreign keys: each row points at an entity in another table instead of copying it.
Keep your API untouched
Normalization is a client boundary concern. Let the server return whatever nested shape it likes, then run a normalize()transform on arrival (and denormalize() if older components still expect the nested shape).
It's not free

The trade-offs

You gainYou pay
UpdatesChange once, propagates everywhereMore code (normalize / denormalize helpers)
ConsistencySingle source of truthSelectors to re-assemble nested views
RenderingStable references, fewer re-rendersMental overhead for the whole team
Best whenEntities repeat + update oftenOverkill for small, static trees

The four costs in detail — tap each:

The judgment call

When to normalize (and when to skip)

Sort each scenario — is normalization worth it here?

0/6 sorted
A user appears across many posts and gets edited often
A static, read-only marketing page
An e-commerce product shown in cart, list, and checkout, with live price changes
A tiny settings object fetched once
A page that just refetches everything from the server on any change
A chat that streams in new messages incrementally
Practice

Check your understanding

Q1Multiple choice
What does normalizing client state actually mean?
Q2Multiple choice
After flattening into by-id tables, how do you keep the feed in its original order?
Q3Sort each scenario
Normalize, or skip?
Social feed where authors recur and get edited
A one-off, read-only FAQ
A dashboard streaming partial entity updates
A small config object, fetched once, never mutated
Try it yourself

Design challenge

Take a Trello-style board (boards → lists → cards, with members):

  1. Write the nested shape, then the normalized tables (byId + allIds).
  2. Show how renaming a member updates everywhere with one write.
  3. Decide honestly: is this app update-heavy enough to justify it?
Key takeaways
  • 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.
← Previous
14. Icon Rendering — Part 3
Next →
16. Real-time Updates — Introduction