Module 6: Global State and App Architecture
Where State Should Live
The different kinds of state in a real app, how to decide where each one belongs, and why most apps need less global state than people think.
Before you pick a state library, you need a way to decide what even counts as state worth managing. The most common architecture mistake I see is the one where a team installs Redux on day one and puts everything into it: form inputs, whether a dropdown is open, data from the server. Then they spend months keeping it all consistent.
The better approach is the other direction. Start with the smallest scope that works, and only widen it when something forces you to.
Not all state is the same#
When someone says "state", they might mean five very different things.
1. Local UI state. Is this dropdown open? Which tab is selected? What's typed in this one input? Only one component cares. Use useState, and keep it there.
2. Shared UI state. The sidebar is collapsed, a modal is open, the theme is dark. A few distant components need it. Lift it up, or use context.
3. Server state. Data that lives on a server and that you borrow a copy of: products, the current user's profile, a list of orders. It can be stale, it can be refetched, someone else can change it. This is a completely different problem from UI state, and a library such as TanStack Query handles it far better than Redux or a hand-written store. We'll cover it in module 7.
4. URL state. Current page, selected product id, search filters, sort order. If it should survive a refresh or be shareable, put it in the URL (React Router's params and search params).
5. Form state. Values, errors and touched flags while editing. Keep it inside the form, or use React Hook Form.
Once you sort state into these buckets, "global state" turns out to be the smallest one, usually things like the current user, theme, and a cart. Many apps I've worked on had only a handful of truly global values, and most of the remaining state was server data wearing a disguise.
The ladder#
When a new piece of state shows up, I climb these steps only as far as needed.
1. useState in the component that uses it
│ another component needs it?
▼
2. Lift it to the closest common parent, pass via props
│ too many layers in between?
▼
3. Restructure with children / slots, or use context
│ many consumers, frequent updates, complex logic?
▼
4. External store (Zustand, Redux Toolkit)
Each step costs a bit more indirection. Skipping straight to step 4 for a value that three nearby components share is paying for something you don't need.
Lifting state up#
Two sibling components need the same data? Their parent owns it.
function Shop() {
const [cart, setCart] = useState([]);
function addToCart(product) {
setCart((c) => [...c, product]);
}
return (
<>
<Header count={cart.length} />
<ProductGrid onAdd={addToCart} />
</>
);
}
Header needs the count, ProductGrid needs a way to add. Neither owns the data. Shop does, and hands each exactly what it needs.
Colocation#
The reverse rule is just as important. If state is only used by one subtree, keep it in that subtree. A common mistake is moving a piece of state to the app root "in case something else needs it later", and then every update to it re-renders the world.
A decent test: if I deleted this component, would this state still need to exist? If not, it belongs inside it.
Derive, don't duplicate#
We've said it a few times already, and it's worth repeating in the architecture context. Don't store a value you can compute.
// Two sources of truth that can disagree
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
// One source of truth
const [items, setItems] = useState([]);
const itemCount = items.length;
Store IDs rather than whole objects when you can: keep selectedId rather than selectedProduct. If the product gets updated in the list, the copy in selectedProduct goes stale. With the id you always look up the current one.
const [selectedId, setSelectedId] = useState(null);
const selected = products.find((p) => p.id === selectedId);
When context stops being enough#
Context is great for low-frequency values that many components read. It begins to hurt when:
- the value changes often, and every consumer re-renders each time
- you want components to subscribe to just a slice of a big object
- the update logic is complex enough that you want tooling to inspect what happened (time travel, action logs)
- you need to read or update state outside React, like in a utility function or an API interceptor
Those are the cases the next two lessons address.
A feature-based structure#
As an app grows, organize code by feature rather than by file type. Everything related to the cart lives together.
src/
├── app/
│ ├── store.js global store setup
│ └── routes.jsx
├── features/
│ ├── cart/
│ │ ├── CartPage.jsx
│ │ ├── CartItem.jsx
│ │ ├── cartSlice.js (or cartStore.js)
│ │ └── useCart.js
│ ├── products/
│ └── auth/
├── components/ truly shared UI (Button, Modal)
├── hooks/ truly shared hooks
└── lib/ api client, formatters
If a feature folder starts importing deeply from another feature folder, that's a design smell. Move the shared piece up into components, hooks or lib.
A decision guide#
- Is it only used in one component? →
useState. - Do a couple of nearby components share it? → lift it.
- Is it from a server? → TanStack Query.
- Should it survive refresh or be shareable? → URL.
- Is it small, rarely changing, and needed widely (theme, user, locale)? → Context.
- Is it shared widely, updated often, and complex? → Zustand or Redux Toolkit.
Where people get burned#
- Putting everything in a global store from day one.
- Copying server data into a Redux store and then writing code to refetch and sync it by hand.
- Duplicating derived values.
- Storing whole objects when an id would do.
- Lifting state to the root "just in case".
- Using context for values that update on every keystroke.
Practice#
- Pick an app you've built (or a screenshot of one you like) and list ten pieces of state in it. Sort each into one of the five kinds above, and say where it should live.
- Take a component that stores both
productsandfilteredProductsand refactor it to derive the second. - Refactor a component that stores
selectedProductto store onlyselectedId. Explain what bug this prevents. - Draw the folder structure for a small blog app with auth, posts and comments, using the feature layout.