State Management in 2025: Zustand vs Jotai vs Redux Toolkit
Compare modern React state management libraries. Zustand offers simplicity, Jotai provides atomic updates, and Redux Toolkit delivers enterprise-grade...
The State of State Management
React's built-in state (useState, useReducer, useContext) works for simple apps, but medium-to-large applications need dedicated state management. In 2025, three libraries dominate: Zustand, Jotai, and Redux Toolkit.
Neural network architecture: data flows through input, hidden, and output layers.
Zustand: Simple and Powerful
Zustand (German for "state") is a small, fast state management library with a hook-based API. No providers, no boilerplate.
Setup
npm install zustand
Creating a Store
// stores/useAuthStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface User {
id
name
email
}
interface AuthState {
user: User | null;
token | null;
isAuthenticated: boolean;
login: (email, password) => Promise<void>;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email, password) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: { "Content-Type": "application/json" },
});
const data = await response.json();
set({
user: data.user,
token: data.token,
isAuthenticated: true,
});
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
},
}),
{ name: "auth-storage" }
)
);
Using the Store
Get more insights on Tutorials
Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.
// components/Header.tsx
function Header() {
const { user, isAuthenticated, logout } = useAuthStore();
if (!isAuthenticated) return <LoginButton />;
return (
<header>
<span>Welcome, {user?.name}</span>
<button
</header>
);
}
// Only re-renders when user changes (selector)
function UserName() {
const name = useAuthStore((state) => state.user?.name);
return <span>{name}</span>;
}
Advanced: Slices Pattern
// Split large stores into slices
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id) => void;
total: () => number;
}
interface UISlice {
sidebarOpen: boolean;
theme: "light" | "dark";
toggleSidebar: () => void;
setTheme: (theme: "light" | "dark") => void;
}
// Combine slices
export const useStore = create<CartSlice & UISlice>()((...a) => ({
...createCartSlice(...a),
...createUISlice(...a),
}));
Jotai: Atomic State
Jotai (Japanese for "state") uses atoms — tiny independent pieces of state that can be composed. It is React-centric and works with Suspense.
Setup
npm install jotai
RAG architecture: user prompts are embedded, matched against a vector store, then fed to an LLM with retrieved context.
Basic Atoms
// atoms/auth.ts
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
interface User {
id
name
email
}
// Primitive atoms
export const userAtom = atomWithStorage<User | null>("user", null);
export const tokenAtom = atomWithStorage<string | null>("token", null);
// Derived atom (computed)
export const isAuthenticatedAtom = atom((get) => {
return get(userAtom) !== null && get(tokenAtom) !== null;
});
// Async atom
export const userProfileAtom = atom(async (get) => {
const token = get(tokenAtom);
if (!token) return null;
const response = await fetch("/api/profile", {
headers: { Authorization: "Bearer " + token },
});
return response.json();
});
// Write-only atom (action)
export const loginAtom = atom(null, async (get, set, credentials: { email password }) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
});
const data = await response.json();
set(userAtom, data.user);
set(tokenAtom, data.token);
});
export const logoutAtom = atom(null, (get, set) => {
set(userAtom, null);
set(tokenAtom, null);
});
Using Atoms
import { useAtom, useAtomValue, useSetAtom } from "jotai";
function Header() {
const isAuthenticated = useAtomValue(isAuthenticatedAtom);
const user = useAtomValue(userAtom);
const logout = useSetAtom(logoutAtom);
if (!isAuthenticated) return <LoginButton />;
return (
<header>
<span>Welcome, {user?.name}</span>
<button => logout()}>Logout</button>
</header>
);
}
Redux Toolkit: Enterprise Standard
Redux Toolkit (RTK) simplifies Redux with opinionated defaults, built-in Immer, and RTK Query for data fetching.
Setup
npm install @reduxjs/toolkit react-redux
Store Setup
// store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
interface AuthState {
user: User | null;
token | null;
status: "idle" | "loading" | "failed";
error | null;
}
const initialState: AuthState = {
user: null,
token: null,
status: "idle",
error: null,
};
export const login = createAsyncThunk(
"auth/login",
async (credentials: { email password }) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
});
return response.json();
}
);
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.status = "loading";
})
.addCase(login.fulfilled, (state, action) => {
state.status = "idle";
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "Login failed";
});
},
});
export const { logout } = authSlice.actions;
export default authSlice.reducer;
Store Configuration
Free Resource
Free Cloud Architecture Checklist
A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.
// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import cartReducer from "./slices/cartSlice";
export const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Comparison
| Feature | Zustand | Jotai | Redux Toolkit |
|---|---|---|---|
| Bundle Size | 1.1KB | 2.4KB | 11KB |
| Boilerplate | Minimal | Minimal | Moderate |
| Provider Required | No | Optional | Yes |
| DevTools | Yes (via middleware) | Yes (plugin) | Yes (built-in) |
| Middleware | Yes | Limited | Extensive |
| Server State | No (use React Query) | Async atoms | RTK Query |
| Learning Curve | Low | Low | Medium |
| Best For | Most apps | Fine-grained updates | Large teams |
ML pipeline: from raw data collection through training, evaluation, deployment, and continuous monitoring.
When to Use What
- Zustand: Best default choice. Simple API, no providers, works everywhere. Choose this unless you have a specific reason not to.
- Jotai: Best when you need fine-grained reactivity (large forms, real-time data, complex derived state). Atoms prevent unnecessary re-renders naturally.
- Redux Toolkit: Best for large teams with complex business logic, when you need time-travel debugging, and when RTK Query can replace your data-fetching layer.
At TechSaaS, we recommend Zustand for most new projects. Its simplicity-to-power ratio is unmatched. For complex dashboard applications with lots of derived state, Jotai's atomic model shines.
Need help with frontend architecture? Contact [email protected].
Related Service
Cloud Solutions
Let our experts help you build the right technology strategy for your business.
Need help with tutorials?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.
We Will Build You a Demo Site — For Free
Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.
No spam. No contracts. Just a free demo.