filipovici bogdan

2024-02-18 · 6 min read

why choose zustand over redux toolkit?

react
state management
zustand
redux

state management in react applications has evolved significantly, and while redux toolkit has been the go-to solution for many years, zustand has emerged as a compelling alternative. let's explore why you might want to choose zustand for your next project.

the rise of zustand

zustand has gained popularity due to its simplicity and minimal boilerplate compared to redux toolkit. while redux toolkit has made significant improvements over vanilla redux, zustand takes simplicity to the next level.

key advantages of zustand

1. minimal boilerplate

// redux toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
 
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 },
  },
});
 
const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});
 
// zustand (v5 — default export was dropped, use the named import)
import { create } from 'zustand';
 
interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
}
 
// the curried `create<T>()(...)` form gives the best type inference
const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

2. no provider required

  • redux requires wrapping your app with provider
  • zustand works out of the box without providers

3. better typescript support

  • first-class typescript support
  • simpler type inference
  • less type-related boilerplate

4. bundle size

  • zustand: ~1kb (minified + gzipped)
  • redux toolkit: ~14kb (minified + gzipped)

when to choose zustand

1. small to medium applications

  • quick to set up
  • less complexity
  • faster development

2. modern react projects

  • works great with react 18+
  • better hooks integration
  • uses useSyncExternalStore under the hood, so it stays tearing-free under concurrent rendering

3. typescript projects

  • better type inference
  • less type boilerplate
  • more intuitive api

best practices with zustand

1. organize store logic using the slices pattern

import { create, StateCreator } from 'zustand';
 
interface AuthSlice {
  user: { id: string } | null;
  login: (user: { id: string }) => void;
  logout: () => void;
}
 
interface SettingsSlice {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}
 
type AppState = AuthSlice & SettingsSlice;
 
const createAuthSlice: StateCreator<AppState, [], [], AuthSlice> = (set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
});
 
const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice> = (
  set,
) => ({
  theme: 'light',
  toggleTheme: () =>
    set((state) => ({
      theme: state.theme === 'light' ? 'dark' : 'light',
    })),
});
 
// combine slices under one store
const useAppStore = create<AppState>()((...args) => ({
  ...createAuthSlice(...args),
  ...createSettingsSlice(...args),
}));

2. use middleware

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
 
interface CounterState {
  count: number;
  increment: () => void;
}
 
const useCounterStore = create<CounterState>()(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
      }),
      { name: 'counter-store' },
    ),
  ),
);

3. selective updates

// only re-renders when `count` changes — avoid selecting the whole state
const count = useCounterStore((state) => state.count);
 
// for multiple fields, pair `useShallow` with a combined selector
import { useShallow } from 'zustand/react/shallow';
 
const { bears, fishes } = useAppStore(
  useShallow((state) => ({ bears: state.bears, fishes: state.fishes })),
);

when to still consider redux toolkit

1. large enterprise applications

  • more structured approach
  • better for large teams
  • established patterns

2. complex state logic

  • built-in middleware system
  • more sophisticated dev tools
  • better debugging capabilities

3. time-travel debugging

  • more mature dev tools
  • better debugging experience

conclusion

zustand offers a refreshing approach to state management that aligns well with modern react development. its simplicity, minimal boilerplate, and excellent typescript support make it an attractive choice for many projects.

while redux toolkit remains a solid choice for large, complex applications, zustand's pragmatic approach and developer-friendly api make it the better choice for many modern react applications. the decision ultimately depends on your specific needs, team size, and project complexity.