When Do You Need State Management?
Not every app needs a state management library. Most state starts local and only needs to be elevated when shared. Follow this progression before reaching for a library.
Context API
Built into React — no extra dependency. Ideal for values that change infrequently and are consumed by many components at different depths. Wrapping the tree in a Provider is all the setup required.
// ThemeContext.tsx
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>
);
}
// Consuming the context
export function Header() {
const theme = useContext(ThemeContext);
return <header data-theme={theme}>...</header>;
}Redux Toolkit
Redux Toolkit (RTK) is the official, opinionated way to write Redux. It eliminates the boilerplate of vanilla Redux with createSlice, createAsyncThunk, and Immer-powered mutable reducers.
// counterSlice.ts
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// Component
function Counter() {
const count = useAppSelector((s) => s.counter.value);
const dispatch = useAppDispatch();
return <button onClick={() => dispatch(increment())}>{count}</button>;
}Zustand
Zustand is a minimal state library — a single create() call returns a hook. No Provider, no boilerplate. The store is just a function that holds state and actions together.
// store.ts
import { create } from "zustand";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
}));
// Component — no Provider needed
function Counter() {
const { count, increment } = useCounterStore();
return <button onClick={increment}>{count}</button>;
}| Zustand | Redux Toolkit |
|---|---|
| ~1 KB bundle | ~11 KB bundle |
| No Provider required | Requires <Provider> at root |
| Minimal boilerplate | Slice + store + hooks setup |
| No DevTools by default | First-class DevTools support |
| Great for small–medium apps | Great for large, team-scale apps |
Choosing the Right Tool
There is no universally correct answer — pick based on team size, update frequency, and how much tooling overhead you can justify.
| Criteria | Best Choice |
|---|---|
| Infrequent global values (theme, locale) | Context API |
| Simple shared state, small team | Zustand |
| Complex async, large team, DevTools | Redux Toolkit |
| Server state (data fetching, caching) | TanStack Query / RTK Query |
| Form state | React Hook Form / Formik |