Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 17 of 38

Choosing a good state structure

Design React state that is hard to get wrong: group related values, avoid contradictions and duplication, derive values in render and keep data flat.

16 minState and eventsUpdated 24 October 2026

By the end of this lessonDecide what belongs in state and what should be calculated, and restructure state to avoid contradictory or duplicated data.

You know how to hold and update state. The next skill is deciding what to put in state and in what shape. A good structure makes components short and removes whole kinds of bugs. A bad one leads to code that keeps values in sync by hand, and eventually fails. Five principles cover most cases.

1. Group state that changes together

If two values always change at the same time, merge them into one object rather than keeping two states in step:

jsx
import { useState } from "react";

function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  function move() {
    setPosition({ x: position.x + 10, y: position.y + 5 });
  }

  return (
    <div>
      <button onClick={move}>Move</button>
      <p>x = {position.x}, y = {position.y}</p>
    </div>
  );
}

export default App;
result
Move
x = 10, y = 5

Separate values that are changed independently into separate states. A form with many fields is a good place to use one object ({ name, email, city }) with a single update function, as the forms lessons show.

2. Avoid contradictions in state

Suppose you track a message that is being sent with two booleans:

jsx
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

Four combinations are possible, but one of them (isSending and isSent both true) makes no sense, and a bug in one handler can produce it. The fix is to use one value that can only be one thing at a time:

jsx
import { useState } from "react";

function App() {
  const [status, setStatus] = useState("typing");     // "typing" | "sending" | "sent"

  function handleSend() {
    setStatus("sending");
    setTimeout(() => setStatus("sent"), 800);
  }

  return (
    <div>
      {status === "typing" && <button className="send" onClick={handleSend}>Send</button>}
      {status === "sending" && <p>Sending...</p>}
      {status === "sent" && <p>Message sent! <button onClick={() => setStatus("typing")}>Write another</button></p>}
    </div>
  );
}

export default App;
result
Message sent! Write another

Now only one state is possible at a time, and each branch of the JSX matches exactly one status. Loading screens follow the same pattern: "idle" | "loading" | "success" | "error" rather than isLoading and hasError and data all at once.

3. Avoid redundant state

If you can calculate a value from props or other state, do not store it. Calculate it while rendering:

jsx
import { useState } from "react";

function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const fullName = (firstName + " " + lastName).trim();    // calculated, not stored

  return (
    <div>
      <input className="first" value={firstName} onChange={e => setFirstName(e.target.value)} placeholder="First name" />
      <input className="last" value={lastName} onChange={e => setLastName(e.target.value)} placeholder="Last name" />
      <p>Full name: {fullName}</p>
    </div>
  );
}

export default App;
result
Full name: Asha Rao

Storing fullName in a third state would force every handler to remember to update it too, and sooner or later one would forget. With a calculation during render, there is a single source of truth and nothing to keep in sync. The same applies to totals, counts and filtered lists:

jsx
import { useState } from "react";

function App() {
  const [items] = useState([
    { id: 1, name: "Pen", price: 10, qty: 3 },
    { id: 2, name: "Notebook", price: 45, qty: 1 },
  ]);

  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);   // derived
  const count = items.length;                                                   // derived

  return <p>{count} products, total {total}</p>;
}

export default App;
result
2 products, total 75

A related trap is copying props into state: const [name, setName] = useState(props.name) captures the prop only on the first render, and later changes to the prop are ignored. Do this only when you really mean "initial value" (name it initialName).

4. Avoid duplication in state

Storing the same data in two places lets the copies drift apart. A typical case is a list plus the "selected" item:

jsx
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);   // a copy of an object that is also in items

If a user then edits the item in the list, selectedItem still points to the old object. Store the id of the selected item, and look it up each time:

jsx
import { useState } from "react";

function App() {
  const [items, setItems] = useState([
    { id: 1, title: "Apples" },
    { id: 2, title: "Bananas" },
  ]);
  const [selectedId, setSelectedId] = useState(1);

  const selected = items.find(item => item.id === selectedId);   // looked up, not copied

  function rename() {
    setItems(items.map(item => (item.id === selectedId ? { ...item, title: item.title + "!" } : item)));
  }

  return (
    <div>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title} <button className="pick" onClick={() => setSelectedId(item.id)}>Select</button>
          </li>
        ))}
      </ul>
      <p>Selected: {selected.title}</p>
      <button className="rename" onClick={rename}>Rename selected</button>
    </div>
  );
}

export default App;
result
Apples Select
Bananas! Select
Selected: Bananas!
Rename selected

After selecting Bananas and renaming it, "Selected" shows the new title "Bananas!" because it is read from the single list. With a stored copy, it would have kept showing "Bananas".

5. Keep state flat

Deeply nested state is hard to update: every change needs a copy at each level. When data is a tree (comments with replies, a folder structure), consider a flat representation: store items in a list or an object by id, with parent ids or arrays of child ids. Each item can then be updated with one map:

jsx
// nested: hard to update a reply three levels down
{ id: 1, text: "A", replies: [{ id: 2, text: "B", replies: [{ id: 3, text: "C", replies: [] }] }] }

// flat: update any item with one map; the tree is built when rendering
[
  { id: 1, text: "A", parentId: null },
  { id: 2, text: "B", parentId: 1 },
  { id: 3, text: "C", parentId: 2 },
]

Server data often arrives nested; you can flatten it when you receive it. Not every case needs this, but if you find yourself writing three levels of spread in a handler, it is a signal to rethink the shape.

Where state should live

Besides its shape, think about who owns each piece of state. Put it in the lowest component that needs it, and move it up only when a sibling also needs it (the next lesson). Do not put everything in App "just in case": it makes every change re-render the whole tree and hides which component really needs what.

A checklist for each piece of state

Ask of each state variable:

  1. Does it change over time? If it never changes, it is a constant or a prop, not state.
  2. Is it passed in as a prop? Then it is the parent's state, not yours.
  3. Can I compute it from other state or props? Then do not store it.
  4. Does it duplicate another state? Store ids rather than copies.
  5. Can it hold a combination that makes no sense? Merge into one status.
  6. Is it deeply nested? Flatten it if updates are painful.

Common mistakes

  • Storing derived values (full name, totals, filtered lists) in state.
  • Using several booleans that can contradict (isLoading, isError, isDone).
  • Copying props into state, and then wondering why updates are ignored.
  • Keeping a copy of the selected object instead of its id.
  • Keeping state in App that only one small component uses.
  • Mirroring the shape of a server response in state without checking whether it is easy to update.

Exercise

  1. Replace the three booleans isLoading, isSuccess, isError in an imaginary component with one status state and write the JSX for each status.
  2. Make a product list with quantity buttons, and show the total price as a derived value, not a state.
  3. Add a "filter" input to a list and show the filtered items by calculating them in render. Check that there is no filteredItems state.
  4. Rewrite a nested comments structure as a flat array with parentId, and write a function that returns the replies of a comment.

Key points

  • Keep the minimum state you need, and calculate everything else from it while rendering.
  • Group values that change together, and use a single status value instead of several booleans that can contradict each other.
  • Avoid redundant state (a full name stored beside first and last name) and duplicated data (a selected object as well as the list it came from): store ids instead.
  • Flat state is easier to update than deeply nested state.

Check yourself

1. A component stores firstName, lastName and fullName in state. What is the problem?
2. Why is a single status ('typing', 'sending', 'sent') often better than isSending and isSent booleans?
3. Instead of storing the selected item object, what should you store?
4. Which state shape is easiest to update?