DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Zustand vs Redux Toolkit: State Management for Modern React Apps

Zustand vs Redux Toolkit: State Management for Modern React Apps

Redux Toolkit fixed most of Redux's problems. Zustand is simpler by design.
Here's when to use each.

The Problem Both Solve

Client-side global state: user auth, UI state, cached server data that multiple components need.

Zustand: Minimal Boilerplate

import { create } from 'zustand'

interface AuthStore {
  user: User | null
  token: string | null
  login: (user: User, token: string) => void
  logout: () => void
}

export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,
  login: (user, token) => set({ user, token }),
  logout: () => set({ user: null, token: null }),
}))

// Usage in any component
function Header() {
  const { user, logout } = useAuthStore()
  return user ? <button onClick={logout}>{user.name}</button> : <LoginButton />
}
Enter fullscreen mode Exit fullscreen mode

That's the entire store. No actions, reducers, or providers.

Redux Toolkit: Explicit and Powerful

import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit'

interface AuthState {
  user: User | null
  token: string | null
  status: 'idle' | 'loading' | 'failed'
}

const authSlice = createSlice({
  name: 'auth',
  initialState: { user: null, token: null, status: 'idle' } as AuthState,
  reducers: {
    loginSuccess: (state, action: PayloadAction<{ user: User; token: string }>) => {
      state.user = action.payload.user
      state.token = action.payload.token
    },
    logout: (state) => {
      state.user = null
      state.token = null
    },
  },
})

export const { loginSuccess, logout } = authSlice.actions

export const store = configureStore({
  reducer: { auth: authSlice.reducer },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Enter fullscreen mode Exit fullscreen mode

More code, but more structure.

Async Operations

Zustand:

export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  loading: false,
  error: null,

  login: async (email: string, password: string) => {
    set({ loading: true, error: null })
    try {
      const { user, token } = await api.login(email, password)
      set({ user, token, loading: false })
    } catch (err) {
      set({ error: err.message, loading: false })
    }
  },
}))
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit (createAsyncThunk):

export const loginUser = createAsyncThunk(
  'auth/login',
  async ({ email, password }: LoginParams) => {
    const { user, token } = await api.login(email, password)
    return { user, token }
  }
)

const authSlice = createSlice({
  // ...
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => { state.status = 'loading' })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.user = action.payload.user
        state.token = action.payload.token
        state.status = 'idle'
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  },
})
Enter fullscreen mode Exit fullscreen mode

RTK automatically handles the loading/success/error lifecycle.

Persistence

Zustand with localStorage:

import { persist } from 'zustand/middleware'

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({ /* ... */ }),
    { name: 'auth-storage' }
  )
)
Enter fullscreen mode Exit fullscreen mode

Done in 5 lines. RTK requires redux-persist and more configuration.

DevTools

Both integrate with Redux DevTools:

// Zustand
import { devtools } from 'zustand/middleware'
const useStore = create(devtools((set) => ({ /* ... */ })))

// RTK — built in automatically
const store = configureStore({ reducer: { /* ... */ } })
Enter fullscreen mode Exit fullscreen mode

When to Choose Each

Choose Zustand when:

  • You want minimal boilerplate
  • Your state is relatively flat
  • Small-to-medium app
  • You want zero provider setup

Choose Redux Toolkit when:

  • Large team with shared state conventions
  • Complex async flows with many loading states
  • You need time-travel debugging heavily
  • Existing Redux codebase migration

My Recommendation

New projects: Zustand for local state + React Query for server state.
This combination covers 95% of what Redux was used for, with a fraction of the code.


The AI SaaS Starter Kit ships with Zustand pre-configured for auth state and React Query for all server data. $99 one-time — no wiring required.

Top comments (0)