Note When two components need the same data, move the state to their closest shared parent and pass it down as props. This is the fundamental React data flow pattern.
lift state upshared statesibling communicationcommon ancestorstate in parent
Prop Drilling Solutions
syntax
// 1. Context// 2. Composition (pass components, not data)// 3. Component composition with children
example
// PROBLEM: drilling "user" through many layers// <App user={user}> -> <Layout user> -> <Header user> -> <Avatar user>// SOLUTION 1: Contextconst UserContext = createContext(null);
// provide at top, consume at Avatar// SOLUTION 2: Composition -- pass the rendered elementfunction App({ user }) {
const avatar = <Avatar user={user} />;
return <Layout header={<Header avatar={avatar} />} />;
}
// Now Layout and Header don't need to know about "user" at allfunction Layout({ header }) {
return (
<div>
<nav>{header}</nav>
<main>...</main>
</div>
);
}
function Header({ avatar }) {
return <div className="header">{avatar}</div>;
}
Note Before reaching for context, try component composition. Passing pre-rendered elements as props skips intermediate layers entirely and keeps the data flow explicit. Context is better for truly global data (theme, auth, locale).
prop drillingpass data deepavoid prop drillingcomponent compositioncontext vs composition
Context for Global State
syntax
// Create -> Provide -> Consumeconst Ctx = createContext(defaultValue);
<Ctx.Provider value={value}>...
const value = useContext(Ctx);
Note Context is ideal for low-frequency updates like theme, locale, and auth. For high-frequency updates (e.g., mouse position), context can cause excessive re-renders -- consider useSyncExternalStore or a dedicated state library.
global statecontext state managementlocale contexti18n reactapp-wide state
When to Use External State
syntax
// Consider external state management when:// - Many components read/write the same state// - State updates are frequent and complex// - You need middleware (logging, persistence, devtools)// - Server state caching is needed (React Query, SWR)
example
// Signs you might need a state library:// 1. Context re-renders are a bottleneck// function App() {// return (// <StoreProvider> {/* every consumer re-renders on any change */}// <BigComponentTree />// </StoreProvider>// );// }// 2. You need selectors (subscribe to a slice)// const count = useStore(state => state.count);// 3. You need server state cache with stale-while-revalidate// const { data, isLoading } = useQuery('users', fetchUsers);// Popular choices:// - Zustand: lightweight, hook-based, selectors// - Jotai: atomic state, bottom-up approach// - React Query / TanStack Query: server state management// - Redux Toolkit: complex client state with devtools
Note Start with useState and useReducer + context. Only add an external library when you hit a concrete pain point: performance issues from context re-renders, complex async logic, or the need for devtools and middleware.
external statestate libraryzustandreduxreact querywhen to use state management
Derived State
syntax
// Compute from existing state during renderconst derivedValue = computeFrom(stateA, stateB);
// Do NOT store derived values in separate state
Note If you can compute a value from existing state or props, derive it inline during render instead of syncing it with useState + useEffect. Derived state in useEffect causes an extra render cycle and introduces potential bugs.
derived statecomputed valuecalculate from stateavoid redundant statesingle source of truth
State Colocation
syntax
// Keep state as close to where it is used as possible// Move state DOWN if only one child needs it// Move state UP only when siblings need to share it
Note Colocating state reduces unnecessary re-renders and makes components easier to understand. Only lift state when it genuinely needs to be shared. Global state for local concerns is an anti-pattern that hurts performance and readability.
state colocationwhere to put statelocal statestate placementstate organization