If you've been working with React for a while, you know the state management landscape can get overwhelming fast. Zustand, Jotai, Recoil, Redux Toolkit, atoms, proxies, signals—you name it.
But here’s the thing:
I’m still using React’s built-in Context API with useReducer—and it’s working just fine.
I want to show you how I structure it, why it still holds up in 2025, and what I’ve learned by keeping things simple.
Why I Haven’t Switched to Zustand, Redux, or Jotai
I’ve played around with a lot of state management libraries over the years. They’re impressive in their own ways. Zustand is sleek. Jotai is elegant. Redux (especially with Toolkit) has matured a lot.
But at the end of the day, my projects just haven’t needed the extra overhead.
Context and useReducer give me:
- Full control and transparency
- No new dependencies or mental models
- Easy debugging and testing
- Just enough flexibility for most app-scale needs
For example, here’s exactly how I manage my shopping bag state in a React app:
import { createContext, Dispatch, FC, ReactNode, useContext, useEffect, useMemo, useReducer, } from "react"; interface BagItem { id: string; name: string; quantity: number; price: number; } interface BagState { bag: BagItem[]; checkoutIsOpen: boolean; } interface BagContext { state: BagState; setState: Dispatch<Partial<BagState>>; } const initialState: BagState = { bag: [], checkoutIsOpen: false, }; export const BagContext = createContext<Partial<BagContext>>({}); export const useBag: () => Partial<BagContext> = () => useContext(BagContext); interface BagProviderProps { children: ReactNode; } const BagProvider: FC<BagProviderProps> = ({ children }) => { const [state, setState] = useReducer( (oldState: BagState, newState: Partial<BagState>) => ({ ...oldState, ...newState, }), initialState ); useEffect(() => { const localBag = localStorage.getItem("bag"); if (localBag) { const payload: BagItem[] = JSON.parse(localBag); setState({ bag: payload }); } }, []); const api = useMemo( (): BagContext => ({ state, setState, }), [state] ); return ( <BagContext.Provider value={api}> {children} </BagContext.Provider> ); }; export default BagProvider; This setup gives me:
- A globally accessible bag state
- A clean useBag() hook
- Persisted state via localStorage
- No external libraries
So Why Not Reach for Something Else?
If I ever find myself dealing with complex derived state, undo/redo functionality, or shared state across iframes/tabs—then sure, I might reach for Zustand or Redux.
But most apps don’t need that.
React’s built-in tools have improved, especially with features like:
- useOptimistic (React 19)
- Server Actions (Next.js)
- React Server Components
And as React continues to shift work to the server, I think we’ll rely less on big client-side state tools—not more.
Final Thoughts
You don’t always need to chase the next hot library.
If Context + hooks do the job—and they often do—stick with them.
This setup has worked for me across a bunch of apps, and until I feel real pain, I’m happy keeping things simple and transparent.
What about you? Are you managing state the old-fashioned way, or have you gone all-in on Jotai, Zustand, or Signals?
Let’s chat—drop a comment with your current stack 👇
Top comments (0)