Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I7
18 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I7 · Part 2 · Intermediate

State architecture

Most React problems are state in the wrong place, in the wrong shape, or duplicated.

4 min read7 sections2 exercises
18 of 57

In this chapter

  • Five kinds of state
  • The three questions before adding state
  • Shape it so bad states cannot exist
  • Normalise when things reference each other
  • Colocation: the default that keeps apps fast

and 2 more

about 4 min
07On this page
  1. Five kinds of state
  2. The three questions before adding state
  3. Shape it so bad states cannot exist
  4. Normalise when things reference each other
  5. Colocation: the default that keeps apps fast
  6. A decision tree
  7. The refactor that fixes most apps

Five kinds of state

"State management" is not one problem. Naming the kind you have usually names the tool.

KindExamplesWhere it belongs
Local UIIs this menu open, which tab, hoveruseState, in the component
FormField values, touched, errorsuseState, or a form library at scale
URLFilters, sort, page, search, the open itemThe URL — useSearchParams
Server cacheAnything fetched from an APIA data library, not useState
Shared clientTheme, auth, cart, a toast queueContext, or a store

The single biggest improvement most React codebases can make is moving rows three and four out of useState. Filters belong in the URL; server data belongs in a cache that knows about staleness.

Server state is not client state

Data from an API is a cached copy of something you do not own. It can be stale, it needs refetching, several components want the same copy, and it has loading and error states. Putting it in useState means reimplementing all of that by hand, badly, in every component that fetches.

The three questions before adding state

  1. Can it be derived? If it can be computed from props or other state, compute it. Do not store it.
  2. Does the UI change when it changes? If not, it is a ref.
  3. Who needs it? Put it at the closest common ancestor of everyone who reads it — and no higher.
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);          // ✗ derived — will drift
const [selectedItem, setSelectedItem] = useState(null);  // ✗ a whole copy

const count = items.length;                     // ✓
const [selectedId, setSelectedId] = useState(null);
const selectedItem = items.find((i) => i.id === selectedId);   // ✓ always fresh

Storing the selected object rather than its id is the classic duplication bug: edit the item in the list and the selection still shows the old version, because they are two separate copies now.

Shape it so bad states cannot exist

// ✗ 16 combinations, 4 of them valid
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

// ✓ 4 states, and they are the 4 that exist
const [state, setState] = useState({ status: "idle" });
// { status: "loading" } | { status: "error", error } | { status: "ready", data }

With separate flags, "loading and also has an error" is one forgotten setter away, and the UI that results is genuinely confusing. With a status field it is unreachable. useReducer is the natural home for this once there are more than two transitions.

Normalise when things reference each other

// ✗ the same author object copied into every post
posts: [{ id: 1, author: { id: 9, name: "Ana" } }, ...]

// ✓ one copy, referenced by id
posts: { 1: { id: 1, authorId: 9 } },
authors: { 9: { id: 9, name: "Ana" } }

Renaming Ana now touches one place instead of every post she wrote. Lookups become O(1). This is a database idea and it applies for the same reason: duplicated data drifts.

Do not normalise a list of twenty things you render once. Normalise when the same entity appears in several places, or when you update items by id.

Colocation: the default that keeps apps fast

State at the top of the tree re-renders everything below it. State next to where it is used re-renders one subtree. The instinct to "lift it up in case something else needs it" is what makes an app slow and its components untestable.

function App() {
  const [isMenuOpen, setMenuOpen] = useState(false);   // ✗ whole app re-renders
}

function Menu() {
  const [isOpen, setOpen] = useState(false);           // ✓ only the menu
}

Lift when two components genuinely need the same value; push it back down the moment only one does.

A decision tree

QuestionAnswer
Comes from a server?A data library. TanStack Query.
Should survive a refresh or a shared link?The URL.
Used by one component?useState, right there.
Used by a few nearby components?Lift to the common parent.
Used everywhere, changes rarely?Context.
Used everywhere, changes often?A store with selectors.
Does not affect rendering?A ref, or a module variable.

The refactor that fixes most apps

Take a component with eight useState calls. Usually two of them are derived and can be deleted, two belong in the URL, three are server data that should be one query, and one is genuinely local. Eight becomes two, and the effects that were keeping them in sync disappear with them.

Interview answer, one sentence

"I split state by kind before choosing a tool — server cache, URL, form, local UI, shared client — because most 'state management' problems are server data sitting in useState, or filters that should be in the URL. Then: derive what you can, keep the rest as low as possible, and shape it so impossible combinations cannot be represented."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsFlatten a list to byIdintermediate
  • 9 testsRound-trip back to a listintermediate
All 66 for this level →
I7

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. I6Previous · I6React Router
  2. I7you are here
  3. Next · I8Memoisationmemo, useMemo and useCallback are a cache — and a cache you did not measure is a cost.6 min read

01On this page

7 sections · 0%

  1. 01Five kinds of state
  2. 02The three questions before adding state
  3. 03Shape it so bad states cannot exist
  4. 04Normalise when things reference each other
  5. 05Colocation: the default that keeps apps fast
  6. 06A decision tree
  7. 07The refactor that fixes most apps

[] chapters · t top