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)