CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

Home/Mobile Development/State Management in React Native: Zustand vs Redux Toolkit
Mobile Development

State Management in React Native: Zustand vs Redux Toolkit

A practical comparison of Zustand and Redux Toolkit for React Native apps — when to use each, real-world patterns, async actions, and persistence with MMKV storage.

Sabir KhaloufiSabir KhaloufiJanuary 15, 20263 min read

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:

bash
npm install zustand
typescript
// 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),
    }
  )
)
typescript
// 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:

tsx
// 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
bash
npm install @reduxjs/toolkit react-redux
typescript
// 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:

bash
npm install @tanstack/react-query
typescript
// 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:

bash
npm install react-native-mmkv
typescript
// 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
#react native#state management#zustand#redux#mobile
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

Full-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.

On this page

  • The Landscape in 2026
  • Zustand: The Pragmatic Choice
  • Redux Toolkit: For Complex Apps
  • Server State: Use TanStack Query
  • Persistence with MMKV (Fast Storage)
  • Which to Choose
  • Key Takeaways

Related Articles

Mobile Development

React Native vs Flutter in 2026: Which One Should You Actually Use?

An honest, experience-based comparison of React Native and Flutter in 2026. Covers performance, developer experience, ecosystem, and which one fits different team types.

Sabir KhaloufiJanuary 25, 20264 min read
Mobile Development

Building Your First React Native App: A Complete Beginner's Guide

Step-by-step guide to building your first React Native app — setup, navigation, data fetching, styling, and deploying to a physical device for testing.

Sabir KhaloufiJanuary 20, 20263 min read
Mobile Development

How to Deploy a React Native App to App Store and Google Play

A step-by-step guide to publishing a React Native (Expo) app to the Apple App Store and Google Play Store — from build configuration to store listing and approval.

Sabir KhaloufiJanuary 10, 20264 min read