Crux Read real React/TS snippets, predict the shape bug, and pick the highest-leverage fix — derived state, normalized vs nested, and colocation.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Shape bugs hide in plain code that compiles and renders fine until an update path it forgot to cover fires. Read each snippet, find the second source of truth or the misplaced state, then pick the fix a senior makes first.
Goal
Practise the loop you run when reviewing state code: read the component, spot the value that should be derived or the data shaped wrong for its access pattern, and reach for the structural fix before any library.
Heads-up useCallback memoizes a function identity; it does not address the real problem, which is storing a value you can compute. The reduce belongs inline in render, not behind state.
Heads-up items is a prop passed in — copying it into local state would add yet another source of truth that drifts from the parent. The fix is fewer stored values, not more.
Heads-up Syncing derived state with an effect is a documented anti-pattern: it renders once with the stale value, then again after the effect. Deriving during render removes both the staleness window and the extra render.
Snippet 2 — nested vs normalized
// state.users[].posts[] — users embedded inside each post's author field toofunction renameUser(state, id: string, name: string) { return { ...state, users: state.users.map(u => u.id === id ? { ...u, name } : u), // posts still carry post.author = {...the old user...} };}
Quiz
Completed
A rename updates the users array but the post list still shows the old name. What shape change is the real fix?
Heads-up That patches the symptom and keeps the duplication — every future field that changes needs the same fan-out, and one missed path drifts again. Removing the embedded copies is the durable fix.
Heads-up Freezing prevents mutation, not staleness — the frozen post.author still holds the old name. The problem is duplicated data, which normalization eliminates.
Heads-up These are client-mutated entities, not server cache to refetch. A cache library does not fix a client shape that duplicates one user across many posts; a byId map does.
Snippet 3 — the hoisted, over-broad state
// app storeconst useStore = create((set) => ({ mouseX: 0, mouseY: 0, setMouse: (x, y) => set({ mouseX: x, mouseY: y }),}));function Dashboard() { const { mouseX, mouseY } = useStore(); // subscribes to the whole store return <ExpensiveCharts /* re-renders on every mousemove */ />;}
Quiz
Completed
Why does scrolling the mouse tank this dashboard's frame rate, and what is the fix?
Heads-up The store library is not the bottleneck — over-broad subscription is. A component reading the entire store re-renders on any change; the fix is narrower scope, not a different library.
Heads-up memo helps only if props are stable, but Dashboard itself re-renders on every mousemove because it subscribes to the changing store. Fix the subscription scope first; memo is a band-aid.
Heads-up Mouse position is ephemeral, per-frame UI state — never shareable view-state. Putting it in the URL would thrash history. It should be colocated in the one component that reads it.
Snippet 4 — server data as client state
function Orders() { const [orders, setOrders] = useState<Order[]>([]); useEffect(() => { fetch('/api/orders').then(r => r.json()).then(setOrders); }, []); // also rendered in a sibling <OrdersSummary/> with its own identical fetch}
Quiz
Completed
Two components run this same fetch-into-useState pattern. Which set of problems does the shape create, and what is the fix?
Heads-up A loading flag is the smallest part. The shape problem is treating server cache as client state, which also costs dedupe, shared caching, and invalidation — all of which a cache library provides at once.
Heads-up A global store dedupes the in-memory copy but you still hand-roll fetching, staleness, refetch, and invalidation. The data is server cache; a cache library keyed by request handles all of that.
Heads-up Identical endpoints still mean two network requests, two cache copies, and independent staleness — exactly the drift and waste a server-cache library exists to remove.
Recap
Every shape bug above is read straight from code: an effect syncing a value you can derive (compute in render instead), authors embedded in posts so a rename goes stale (normalize to byId + join), high-frequency state hoisted into a global store that re-renders the world (colocate or select a slice), and a fetch dragged into useState that should be server cache (use a cache library keyed by the request). Read the component, find the second source of truth or the state placed wrong for its readers, and fix the shape before reaching for a library.