useReducer is the right primitive when the next state depends on the previous state, when the state shape is complex (nested objects, multiple sub-fields), or when updates are better expressed as named actions than as scattered setters. A reducer centralizes transitions in a pure function — easier to test, easier to extend with discriminated-union action types that give full TypeScript inference inside the case clauses. Lazy initial state (useReducer(reducer, initArg, init)) lets you defer an expensive computation. Pairing useReducer with Immer lets you write mutating drafts that produce immutable updates ergonomically. Two state-setter patterns are worth knowing everywhere: the lazy initializer useState(() => expensive()) runs the factory only on mount (avoid the trap of useState(expensive()), which runs every render), and the functional updater setCount(c => c + 1) avoids stale-closure bugs when several updates are batched before the next render.
Context is React's built-in way to share state across a subtree, but it has a sharp edge: every consumer re-renders whenever the context value changes. Mitigations include splitting contexts by domain (so an unrelated update doesn't ripple), memoizing the value passed to the provider with useMemo, and adopting a selector pattern via libraries like use-context-selector to subscribe to slices. The classic performance pattern is to split into StateContext and DispatchContext; because dispatch is referentially stable across renders, consumers that only dispatch don't need to re-render when state changes. Lifting state up is still the right call for two or three components that share state — reach for Context only when the scope is broader, and avoid it for high-frequency updates where many consumers would otherwise thrash.
For app-wide state, dedicated stores typically beat hand-rolled Context-plus-reducer. The unifying primitive is useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?), which subscribes to an external store with concurrent-rendering-safe semantics; its getSnapshot must return referentially stable values or React will warn about tearing, and the third argument is required for SSR. Zustand exposes a hook created from a store factory and supports slice selectors that re-render only when that slice changes; Jotai composes primitive atoms into derived atoms; Redux Toolkit's createSlice plus useSelector and useDispatch remains popular in larger apps, especially when paired with shallowEqual so that selecting an object doesn't force a re-render when only one of its keys changed. Compound components follow a related idea: a parent provides a Context, and children read it through a custom hook — perfect for accordions, tabs, and menus where siblings need shared state but a parent prop-drilling would be brittle.