Zustand vs Redux Toolkit: State Management in 2024
Zustand vs Redux Toolkit: State Management in 2024
After building apps with both, here's my unbiased comparison of Zustand and Redux Toolkit (RTK).
Quick Comparison
Feature | Zustand | Redux Toolkit |
---|---|---|
Bundle Size | 8KB | 36KB |
Learning Curve | 5 minutes | 2 hours |
TypeScript | Great | Excellent |
DevTools | Good | Excellent |
Ecosystem | Growing | Massive |
Performance | Excellent | Excellent |
Code Comparison
Simple Counter
Zustand
// store.ts
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
}
export const useCounter = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// Component.tsx
function Counter() {
const { count, increment, decrement } = useCounter()
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
Redux Toolkit
// store.ts
import { configureStore, 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 const store = configureStore({
reducer: { counter: counterSlice.reducer },
})
// Component.tsx
import { useSelector, useDispatch } from 'react-redux'
function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
)
}
Advanced Patterns
Async Actions
Zustand
const useStore = create((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true })
const users = await api.getUsers()
set({ users, loading: false })
},
}))
Redux Toolkit
const usersSlice = createSlice({
name: 'users',
initialState: { list: [], loading: false },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.list = action.payload
state.loading = false
})
},
})
export const fetchUsers = createAsyncThunk(
'users/fetch',
async () => api.getUsers()
)
Performance Deep Dive
Re-render Optimization
Zustand
// Automatic selector optimization
const count = useStore((state) => state.count) // Only re-renders on count change
Redux Toolkit
// Requires createSelector for similar optimization
const selectCount = createSelector(
[(state) => state.counter],
(counter) => counter.value
)
Real-World Scenarios
When to Use Zustand
- Small to medium apps - Less boilerplate
- Rapid prototyping - Get running in minutes
- Component-level state - Replace Context API
- Simple async - Built-in async support
// Perfect Zustand use case: Shopping cart
const useCart = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
total: 0,
calculateTotal: () => set((state) => ({
total: state.items.reduce((sum, item) => sum + item.price, 0)
})),
}))
When to Use Redux Toolkit
- Large applications - Better structure
- Complex state logic - Time travel, middleware
- Team projects - Established patterns
- Heavy async - RTK Query is amazing
// Perfect RTK use case: Complex data fetching
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users',
providesTags: ['User'],
}),
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `users/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['User'],
}),
}),
})
Migration Strategies
From Redux to Zustand
// Gradual migration - use both temporarily
const useHybridStore = () => {
const reduxData = useSelector(state => state.oldFeature)
const zustandData = useNewStore()
return { ...reduxData, ...zustandData }
}
From Zustand to Redux
// Convert Zustand stores to RTK slices
const slice = createSlice({
name: 'feature',
initialState: zustandStore.getState(),
reducers: {
// Map Zustand actions to reducers
}
})
My Recommendations
Choose Zustand When:
- Building SPAs under 50 components
- Need minimal setup
- Working solo or small team
- Performance is critical
- Learning React
Choose Redux Toolkit When:
- Building enterprise applications
- Need extensive middleware
- Working with large teams
- Complex debugging required
- Using RTK Query
Performance Benchmarks
Operation | Zustand | RTK |
---|---|---|
Store creation | 0.1ms | 0.8ms |
Simple update | 0.05ms | 0.08ms |
Selector (1000 items) | 0.2ms | 0.3ms |
DevTools overhead | ~5% | ~8% |
Final Verdict
There's no clear winner. Both are excellent in 2024.
- Zustand: Perfect for 80% of apps. Simple, fast, delightful.
- Redux Toolkit: Best for complex apps needing structure and tooling.
My approach: Start with Zustand. If you need Redux features, you'll know. The migration is straightforward if needed.
Both will serve you well. Pick based on your needs, not hype.
About Ansh Gupta
Frontend Developer with 3 years of experience building modern web applications. Based in Indore, India, passionate about React, TypeScript, and creating exceptional user experiences.
Learn more about meRelated Articles
Testing Like a Pro with Vitest (2025)
Fast, delightful testing for React and TypeScript. Learn how to structure tests, mock networks, and measure coverage with minimal boilerplate.
Vercel Edge Best Practices (2025)
Make your Next.js apps feel instant: edge runtime choices, caching patterns, image strategy, and observability that scales.
Next.js Production Checklist (2025)
A short checklist to ship solid Next.js apps: routing, caching, images, envs, and SEO—no fluff.