AniUI Academy

Normalizing State Shape

Why nested, duplicated state causes real bugs at scale, and how normalizing into flat, ID-keyed structures — the client-side relational model — fixes it.

11 min read

A surprising fraction of "weird UI bug" tickets trace back to the same root cause: the same piece of real-world data exists in more than one place in application state, and an update only reached some of the copies. Normalizing state shape is the fix, and it's directly borrowed from a much older idea — relational database design.

The shape of the problem

Imagine a feed of posts, each fetched with its author embedded:

const posts = [
  { id: "p1", title: "Hello", author: { id: "u1", name: "Anish" } },
  { id: "p2", title: "World", author: { id: "u1", name: "Anish" } },
];

The same author, u1, is duplicated across every post they wrote. If that user updates their display name, every one of those embedded copies needs to be found and updated — and if even one array in state holds a stale copy, the UI shows two different names for the same person, in the same session, depending on which piece of state a given component happened to read.

The normalized shape

Normalizing splits this into two flat, ID-keyed lookup tables, plus references between them:

const state = {
  posts: {
    byId: {
      p1: { id: "p1", title: "Hello", authorId: "u1" },
      p2: { id: "p2", title: "World", authorId: "u1" },
    },
    allIds: ["p1", "p2"],
  },
  users: {
    byId: {
      u1: { id: "u1", name: "Anish" },
    },
    allIds: ["u1"],
  },
};

Now there is exactly one copy of the user record. Updating u1's name is a single write to users.byId.u1, and every post that references authorId: "u1" automatically reflects the change the next time it's read — because there was never a second copy to fall out of sync in the first place.

Nested / denormalized state

Easy to read directly — a post already has its author's name inline. Every duplicate copy is a separate place an update can be missed.

Normalized state

One canonical record per entity, referenced by ID. Writes are safe and singular; reads need an assembly step to reconstruct a component-ready shape.

What you're actually trading

Normalization doesn't make the underlying complexity disappear — it moves it. Writes get simpler and safer: updating an entity is one operation on one record, with no risk of missing a copy. Reads get more work: rendering "a post with its author's name" now requires joining posts.byId against users.byId — usually via a small selector function (selectPostWithAuthor(postId)) rather than reading a field straight off the post. That join is cheap computationally, but it's a real piece of code that has to exist and stay correct, and it means the shape you fetch from an API is rarely the shape you store — there's a normalization step in between.

This trade is worth it exactly when the same entity genuinely appears in multiple places and needs to stay consistent when it changes — a comment section where the same user appears on many comments, a table where the same product appears in several filtered views, a chat app where the same message might be referenced from a thread view and a search result. It's not worth the ceremony for state that's small, read-only, or where duplication never actually causes a consistency bug because nothing ever updates the embedded copy after it's fetched.

Where this shows up in real tooling

This is precisely the model libraries like Redux's createEntityAdapter or Apollo Client's normalized cache implement for you — Apollo, in particular, normalizes GraphQL responses by type and ID automatically, which is exactly why updating one field on one object anywhere in your app updates every component reading that object elsewhere, without you writing any join logic by hand. Recognizing "this is the normalization problem" is often the more valuable interview signal than being able to hand-write the reducer — it's the reasoning that transfers across whichever specific state library a team happens to use.

What to remember

  • Duplicating the same entity across multiple places in state is the direct cause of "the UI shows two different values for the same thing" bugs.
  • Normalizing stores one canonical record per entity, keyed by ID, with everything else holding a reference instead of a copy — the same idea as a database foreign key.
  • The trade is real: writes become simpler and safer, but reads need an assembly/join step to reconstruct a component-ready shape.
  • Reach for normalization when the same entity appears in multiple places and must stay consistent when it changes — not as a default for every piece of state.

Check yourself

3 questions · pass 3/3 to unlock Server State vs. Client State

up to 50
  1. 1.A UI stores a list of blog posts, each with a nested author object embedded directly in it. The same author appears in 40 posts. What's the concrete bug risk this creates?

  2. 2.What does 'normalizing' state typically mean in practice?

  3. 3.What's the real cost normalization trades for consistency?

3 left to answer