No history yet

State Architecture Patterns

The Source of Truth

As applications grow, one of the most common sources of bugs isn't tricky logic, but a simple question: which piece of data is correct? Imagine you have a list of tasks and a separate counter for completed tasks. When a user marks a task as done, you have to update the task itself and increment the counter. If you forget one, your UI is now lying to the user.

This is a synchronization problem. The solution is to store the minimum amount of information necessary and calculate everything else on the fly. We call this essential data the base state. It's the single source of truth. Everything else, like the count of completed tasks, is derived state.

Your base state should be the raw, uncomputed data. In our example, it's just the array of task objects. The number of completed tasks can always be found by filtering that array, so it shouldn't be a separate state variable.

Flattening Complex Data

APIs often send data in a nested format that's convenient for relationships but difficult to manage in a state. For example, a blog post object might contain an array of comment objects, and each comment object might contain a user object. What happens when a user updates their username? You'd have to find and update that user's name in every single comment they've ever made. This is an O(n) update problem, and it's a nightmare to maintain.

The better approach is data normalization, a technique borrowed from database design. You flatten the nested structure into separate "tables" or objects, using IDs to reference relationships. This ensures that each piece of data lives in only one place.

// Before: Nested API Response
const nestedPost = {
  id: 'post1',
  title: 'My Awesome Post',
  author: {
    id: 'user1',
    name: 'Alice'
  },
  comments: [
    {
      id: 'comment1',
      text: 'Great read!',
      author: { id: 'user2', name: 'Bob' }
    },
    {
      id: 'comment2',
      text: 'Thanks for sharing.',
      author: { id: 'user1', name: 'Alice' }
    }
  ]
};

After normalizing, our state would look like this. Notice how each entity (post, user, comment) has its own space. Updating Alice's name now requires changing only one value.

// After: Normalized State
const normalizedState = {
  posts: {
    post1: { 
      id: 'post1', 
      title: 'My Awesome Post', 
      author: 'user1', 
      comments: ['comment1', 'comment2'] 
    }
  },
  comments: {
    comment1: { id: 'comment1', text: 'Great read!', author: 'user2' },
    comment2: { id: 'comment2', text: 'Thanks for sharing.', author: 'user1' }
  },
  users: {
    user1: { id: 'user1', name: 'Alice' },
    user2: { id: 'user2', name: 'Bob' }
  }
};

Computing on Demand

Now that we have a clean, normalized base state, how do we build the rich, nested objects our components need? We derive them. We can write functions that take the base state and reconstruct the data structures required for a specific view.

But what if this reconstruction is computationally expensive? Running a complex filter or transformation on a large dataset on every single render could slow down your application. This is where memoization comes in. By wrapping our derived state calculation in a useMemo hook, we tell React to cache the result. It will only re-run the calculation if one of the dependencies—the pieces of base state it relies on—has changed.

import { useMemo } from 'react';

function PostComponent({ postId, state }) {
  // Derive the full post object with populated comments
  const postWithComments = useMemo(() => {
    const post = state.posts[postId];
    if (!post) return null;

    // Reconstruct the nested structure for the UI
    return {
      ...post,
      author: state.users[post.author],
      comments: post.comments.map(commentId => ({
        ...state.comments[commentId],
        author: state.users[state.comments[commentId].author]
      }))
    };
  }, [postId, state.posts, state.comments, state.users]);

  // ... render the postWithComments
}

In this example, the complex work of re-assembling the post and its comments will only happen if the post itself, the comments, or the users change. If some other part of the app's state updates, this component gets the cached version for free, keeping the UI fast and responsive.

Ready to test your knowledge?

Quiz Questions 1/5

What is "base state" in the context of application development?

Quiz Questions 2/5

An application displays a list of products and a separate counter for the number of items in the shopping cart. To add an item, the developer must update both the cart list and the counter variable. What is the primary problem with this approach?

By defining a clear source of truth, normalizing complex data, and using memoization to derive state efficiently, you can build scalable and bug-resistant React applications. This architectural discipline pays dividends as your project grows in complexity.