React State Management Decision Tree
Choosing the right state tool depends on scope, complexity, and how many components need the data.
Decision Tree
Is it server data (API/DB)?
YES → Use a data-fetching library (TanStack Query, SWR)
NO ↓
Is it used by only one component?
YES → useState or useReducer
NO ↓
Is it shared between parent and child?
YES → Lift state up + props
NO ↓
Is it shared across a subtree (< 10 consumers)?
YES → Context + useReducer
NO ↓
Is it global, frequently updated, or complex?
YES → External library (Zustand, Jotai)
useState
Simple, local state for a single component.
const [ count , setCount ] = useState ( 0 );
const [ form , setForm ] = useState ({ name: "" , email: "" });
Use when: Single value or small object, one component, simple updates.
useReducer
State with complex transitions or multiple sub-values.
type State = { items : Item []; loading : boolean ; error : string | null };
type Action =
| { type : "FETCH_START" }
| { type : "FETCH_SUCCESS" ; items : Item [] }
| { type : "FETCH_ERROR" ; error : string };
function reducer ( state : State , action : Action ) : State {
switch (action.type) {
case "FETCH_START" : return { ... state, loading: true , error: null };
case "FETCH_SUCCESS" : return { items: action.items, loading: false , error: null };
case "FETCH_ERROR" : return { ... state, loading: false , error: action.error };
}
}
Use when: Multiple related state transitions, state machine patterns, testable state logic.
Context
Share state across a component subtree without prop drilling.
const ThemeContext = createContext < "light" | "dark" >( "light" );
export function ThemeProvider ({ children } : { children : ReactNode }) {
const [ theme , setTheme ] = useState < "light" | "dark" >( "light" );
return (
< ThemeContext.Provider value = {theme} >
{ children }
</ ThemeContext.Provider >
);
}
Use when: Theme, locale, auth status — values that change infrequently and are needed by many components.
Avoid when: Frequently updating values (causes re-renders of all consumers).
External Libraries
Zustand — Lightweight global store
import { create } from "zustand" ;
const useStore = create <{ count : number ; inc : () => void }>(( set ) => ({
count: 0 ,
inc : () => set (( s ) => ({ count: s.count + 1 })),
}));
Jotai — Atomic state
import { atom, useAtom } from "jotai" ;
const countAtom = atom ( 0 );
const [ count , setCount ] = useAtom (countAtom);
Use when: Global state, frequent updates, complex derived state, or you need middleware (persist, devtools).
Anti-Patterns
Putting everything in global state: Most state is local. Start with useState .
Context for frequent updates: Use Zustand or Jotai for rapidly changing values.
Duplicating server state: Don't copy API data into useState . Use TanStack Query.
Premature optimization: Start simple, add complexity only when needed.