Follow Us

CodeWithSabir

  • Contact Us
  • Privacy Policy
  • About
  • Terms & Conditions

All Rights Reserved © 2026

  • Light
  • Dark
Mobile Development

State Management in React Native: Zustand vs Redux Toolkit

Sabir Soft
Sabir Lkhaloufi
  • January 15, 2026
  • 3 min read

State Management in React Native: Zustand vs Redux Toolkit

State management in React Native follows the same principles as React on the web, but with extra concerns: offline support, background sync, and persistence across app restarts. Choosing the right tool saves you from major refactoring down the line.

This guide compares Zustand and Redux Toolkit (RTK) in the context of React Native apps, with practical patterns for real-world scenarios.

The Landscape in 2026

For most React Native apps, the choice comes down to:

  • Zustand — minimal, fast, and boilerplate-free
  • Redux Toolkit — structured, powerful middleware ecosystem, better for complex apps
  • React Query / TanStack Query — specifically for server state (API data)
  • Context API — fine for small apps, doesn't scale

The common mistake is treating all state the same. Most apps have two very different types:

  • Server state: data from your API (posts, users, products) — use React Query
  • Client state: UI state, auth tokens, user preferences — use Zustand or RTK

Zustand: The Pragmatic Choice

Zustand requires almost no boilerplate and feels like writing regular JavaScript:

npm install zustand
// store/authStore.ts
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import AsyncStorage from '@react-native-async-storage/async-storage'
 
interface User {
  id: string
  email: string
  name: string
  token: string
}
 
interface AuthState {
  user: User | null
  isAuthenticated: boolean
  login: (user: User) => void
  logout: () => void
  updateUser: (updates: Partial<User>) => void
}
 
export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      isAuthenticated: false,
 
      login: (user) => set({ user, isAuthenticated: true }),
 
      logout: () => set({ user: null, isAuthenticated: false }),
 
      updateUser: (updates) =>
        set((state) => ({
          user: state.user ? { ...state.user, ...updates } : null,
        })),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
)
// store/cartStore.ts
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
 
interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}
 
interface CartState {
  items: CartItem[]
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: string) => void
  updateQuantity: (id: string, quantity: number) => void
  clearCart: () => void
  total: () => number
}
 
export const useCartStore = create<CartState>()(
  immer((set, get) => ({
    items: [],
 
    addItem: (item) =>
      set((state) => {
        const existing = state.items.find(i => i.id === item.id)
        if (existing) {
          existing.quantity += 1
        } else {
          state.items.push({ ...item, quantity: 1 })
        }
      }),
 
    removeItem: (id) =>
      set((state) => {
        state.items = state.items.filter(i => i.id !== id)
      }),
 
    updateQuantity: (id, quantity) =>
      set((state) => {
        const item = state.items.find(i => i.id === id)
        if (item) item.quantity = quantity
      }),
 
    clearCart: () => set({ items: [] }),
 
    total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
  }))
)

Using stores in components:

// screens/ProfileScreen.tsx
import { useAuthStore } from '@/store/authStore'
 
export default function ProfileScreen() {
  const { user, logout } = useAuthStore()
 
  if (!user) return null
 
  return (
    <View>
      <Text>{user.name}</Text>
      <Text>{user.email}</Text>
      <Button title="Sign Out" onPress={logout} />
    </View>
  )
}

Redux Toolkit: For Complex Apps

RTK shines when you need:

  • Complex middleware (logging, analytics, crash reporting)
  • Time-travel debugging with Redux DevTools
  • Very large teams where structure and convention matter
  • Complex async flows with createAsyncThunk
npm install @reduxjs/toolkit react-redux
// store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
 
interface AuthState {
  user: User | null
  status: 'idle' | 'loading' | 'succeeded' | 'failed'
  error: string | null
}
 
export const loginThunk = createAsyncThunk(
  'auth/login',
  async (credentials: { email: string; password: string }, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      })
      if (!response.ok) {
        const error = await response.json()
        return rejectWithValue(error.message)
      }
      return await response.json()
    } catch (error) {
      return rejectWithValue('Network error')
    }
  }
)
 
const authSlice = createSlice({
  name: 'auth',
  initialState: { user: null, status: 'idle', error: null } as AuthState,
  reducers: {
    logout: (state) => {
      state.user = null
      state.status = 'idle'
      state.error = null
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginThunk.pending, (state) => {
        state.status = 'loading'
        state.error = null
      })
      .addCase(loginThunk.fulfilled, (state, action: PayloadAction<User>) => {
        state.status = 'succeeded'
        state.user = action.payload
      })
      .addCase(loginThunk.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.payload as string
      })
  },
})
 
export const { logout } = authSlice.actions
export default authSlice.reducer

Server State: Use TanStack Query

For API data, neither Zustand nor Redux should be your first choice:

npm install @tanstack/react-query
// hooks/usePosts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
 
export function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await fetch('/api/posts')
      if (!res.ok) throw new Error('Failed to fetch')
      return res.json()
    },
    staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
  })
}
 
export function useCreatePost() {
  const queryClient = useQueryClient()
 
  return useMutation({
    mutationFn: async (post: { title: string; content: string }) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(post),
      })
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })
}

Persistence with MMKV (Fast Storage)

AsyncStorage works but is slow for large datasets. MMKV is 10x faster:

npm install react-native-mmkv
// lib/storage.ts
import { MMKV } from 'react-native-mmkv'
import { StateStorage } from 'zustand/middleware'
 
const storage = new MMKV()
 
export const zustandMMKVStorage: StateStorage = {
  getItem: (name) => storage.getString(name) ?? null,
  setItem: (name, value) => storage.set(name, value),
  removeItem: (name) => storage.delete(name),
}
 
// Use in Zustand persist middleware:
// storage: createJSONStorage(() => zustandMMKVStorage)

Which to Choose

ScenarioRecommendation
Simple app, 1-2 devsZustand
API data fetchingTanStack Query
Complex async flowsRTK
Large team, enterpriseRTK
Need DevTools debuggingRTK
Offline-first appZustand + TanStack Query offline

Key Takeaways

  • Split server state (API data) from client state — use different tools for each
  • Zustand is simpler and faster to write; RTK is more structured and better for large teams
  • TanStack Query handles server state better than either Zustand or Redux
  • Use MMKV instead of AsyncStorage for performance-sensitive persistence
  • The Zustand immer middleware makes complex state mutations simple and safe
  • Persist only what you need — persisting too much state causes slow app startup
Popular Blogs
Claude AI vs ChatGPT: An Honest Comparison for Developers
  • April 28, 2026
AI Tools Every Developer Should Be Using in 2026
  • April 20, 2026
Using the Claude API in Real Projects: A Practical Developer Guide
  • April 15, 2026
Prompt Engineering for Developers: Write Prompts That Actually Work
  • April 10, 2026
Categories
AIDevOpsNext.jsMobile DevelopmentWeb Development

Related Posts

Mobile Development
React Native vs Flutter in 2026: Which One Should You Actually Use?
Sabir Khaloufi·Jan 25, 2026
Mobile Development
Building Your First React Native App: A Complete Beginner's Guide
Sabir Khaloufi·Jan 20, 2026
Mobile Development
How to Deploy a React Native App to App Store and Google Play
Sabir Khaloufi·Jan 10, 2026