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.
- #React
- #State Management
- #Zustand
- #Redux Toolkit
- #JavaScript
- #Performance