State Management in React 2025: Redux vs Zustand vs Jotai
Redux vs Zustand vs Jotai compared for React 2025: when to use each, real code examples, and which state management solution fits your app.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
State Management in React 2025: Redux vs Zustand vs Jotai
The most common question I get from developers moving from tutorials to real apps: "Do I need Redux?"
In 2019, the answer was usually yes. In 2025, the answer is almost always no — at least not at first.
React hooks changed state management dramatically. useState handles component-local state. useReducer handles complex state logic. useContext shares state across components. For most apps, that's enough.
But when it's not enough — when you have global state that dozens of components need, when prop drilling becomes painful, when you need cross-component state synchronization — that's when you need a library.
This guide compares the real options in 2025: Redux Toolkit, Zustand, and Jotai. Not theoretical comparison — concrete code showing when each fits.
First: When You Don't Need a Library
Before picking a state management library, consider whether you need one:
// Context + useReducer handles a lot
interface AppState {
user: User | null;
theme: 'light' | 'dark';
}
type Action =
| { type: 'SET_USER'; payload: User }
| { type: 'LOGOUT' }
| { type: 'TOGGLE_THEME' };
function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'SET_USER': return { ...state, user: action.payload };
case 'LOGOUT': return { ...state, user: null };
case 'TOGGLE_THEME': return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
default: return state;
}
}
const AppContext = createContext<{ state: AppState; dispatch: Dispatch<Action> } | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { user: null, theme: 'light' });
return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
}
Context works great for state that changes infrequently (theme, auth, locale). It has one significant problem: every component that consumes the context re-renders when any part of the context changes. For frequently-changing state (filters, UI state, form data), this causes performance problems.
That's when you graduate to a library.
Option 1: Zustand — The Sweet Spot
Zustand is a tiny (1KB), fast state management library with a simple API. It's become the go-to Redux alternative for most React apps in 2025.
npm install zustand
Creating a Store
import { create } from 'zustand';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
total: () => number;
clearCart: () => void;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(i => i.id === id ? { ...i, quantity } : i)
})),
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
clearCart: () => set({ items: [] }),
}));
Using the Store
// Any component, anywhere in the tree — no Provider needed
function CartIcon() {
const items = useCartStore(state => state.items); // Selective subscription
const total = useCartStore(state => state.total);
return (
<div>
<span>{items.length} items</span>
<span>${total().toFixed(2)}</span>
</div>
);
}
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => addItem(product)}>Add to Cart</button>
</div>
);
}
What I love about Zustand: no Provider wrapping, selective subscriptions prevent unnecessary re-renders, and the API is just a function.
Zustand with Persistence
import { persist } from 'zustand/middleware';
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
// ... store definition
}),
{ name: 'cart-storage' } // Key in localStorage
)
);
Option 2: Jotai — Atomic State
Jotai takes a different approach: instead of one store, you have individual atoms that components subscribe to independently.
npm install jotai
Creating Atoms
import { atom } from 'jotai';
// Primitive atoms
export const userAtom = atom<User | null>(null);
export const themeAtom = atom<'light' | 'dark'>('light');
// Derived atom (computed from other atoms)
export const isAdminAtom = atom((get) => {
const user = get(userAtom);
return user?.role === 'admin';
});
// Async atom
export const userPostsAtom = atom(async (get) => {
const user = get(userAtom);
if (!user) return [];
const response = await fetch(`/api/posts?userId=${user.id}`);
return response.json();
});
Using Atoms
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
function UserProfile() {
const [user, setUser] = useAtom(userAtom); // Read + write
const isAdmin = useAtomValue(isAdminAtom); // Read only
return (
<div>
{user ? <p>Hello, {user.name}</p> : <p>Not logged in</p>}
{isAdmin && <AdminPanel />}
</div>
);
}
function LoginButton() {
const setUser = useSetAtom(userAtom); // Write only — no re-render on value change
return (
<button onClick={() => setUser({ id: 1, name: 'Alice', role: 'user' })}>
Login
</button>
);
}
Jotai's strength: components only re-render when the specific atoms they subscribe to change. This makes it extremely efficient for fine-grained updates.
Option 3: Redux Toolkit — When It's Still Worth It
Redux Toolkit (RTK) is the modern way to use Redux. The old boilerplate-heavy Redux is dead — RTK writes 90% of the code for you.
npm install @reduxjs/toolkit react-redux
import { createSlice, configureStore } from '@reduxjs/toolkit';
const notificationsSlice = createSlice({
name: 'notifications',
initialState: { items: [], unread: 0 },
reducers: {
addNotification: (state, action) => {
state.items.unshift(action.payload); // Immer makes mutations safe
state.unread++;
},
markAllRead: (state) => {
state.items.forEach(n => { n.read = true; });
state.unread = 0;
},
},
});
const store = configureStore({
reducer: { notifications: notificationsSlice.reducer }
});
Redux Toolkit makes sense when:
- Your team already knows Redux
- You need Redux DevTools time-travel debugging
- Complex state machines with many interdependent slices
- Large enterprise apps where strict patterns matter
Comparison at a Glance
| Zustand | Jotai | Redux Toolkit | |
|---|---|---|---|
| Bundle size | ~1KB | ~3KB | ~11KB |
| Learning curve | Low | Low-Medium | Medium |
| Boilerplate | Very little | Very little | Some |
| DevTools | Basic | Good | Excellent |
| Provider needed | ❌ | ✅ (optional) | ✅ |
| Best for | Store-based app state | Fine-grained atomic state | Large apps with complex state |
| SSR support | Good | Excellent | Good |
My Recommendation for 2025
Small to medium apps: Use useState + useContext. Add Zustand when you hit re-render performance issues or prop drilling pain.
Medium to large apps: Zustand for client state + React Query/TanStack Query for server state. This combination handles 95% of React apps.
Apps with complex derived state: Jotai is worth the mental model shift.
Existing Redux codebases: Migrate to Redux Toolkit if you haven't — don't rewrite to Zustand unless there's a good reason.
For the React fundamentals that underpin all of this, see our React tutorial for beginners. State management decisions depend on your framework choice too — our React vs Next.js vs Remix comparison covers how server rendering changes the picture. And for the React hooks that manage local state before you need a library, our React hooks tutorial covers them all.
Frequently Asked Questions
Do I need Redux in a React app in 2025?
Probably not. Context + hooks handles most cases. Zustand is a lighter alternative when you do need global state. Redux makes sense for large apps with existing Redux or complex state logic.
What is the difference between Zustand and Jotai?
Zustand: store-based, simple API, great for related state. Jotai: atomic, fine-grained subscriptions, great for derived/computed state.
What state goes global vs local?
Local: form inputs, open/closed UI state, component-specific data. Global: authenticated user, cart, preferences, data shared across many components.
What is React Query?
TanStack Query manages server state — fetching, caching, and syncing. Pair it with Zustand (client state) for a complete state management solution.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
How to Deploy a React App to Vercel in 10 Minutes
Deploy a React app to Vercel in 10 minutes: from npm create vite to live URL, custom domain setup, environment variables, and preview deployments.
GraphQL vs REST: Which API Style Should You Learn in 2025?
GraphQL vs REST API compared honestly for 2025: when each makes sense, real code examples, and which API style to learn first as a developer.
JavaScript Promises and Async/Await: Finally Understand Them
JavaScript async await and Promises explained clearly: the event loop, Promise chains, async/await patterns, error handling, and common mistakes to avoid.
How to Pass a JavaScript Interview at Google, Meta, or Amazon
How to pass a JavaScript interview at top tech companies: closures, event loop, promises, DOM questions, system design, and real interview questions answered.