State Management in React Native: Context vs Zustand vs Redux

12 min read

A comprehensive comparison of state management solutions for React Native, with real-world examples showing when to use each approach.

React NativeState ManagementContextZustandRedux

The State Management Spectrum

Choosing the right state management solution is critical for React Native apps. There's no one-size-fits-all answer—the best choice depends on your app's complexity, team size, and specific requirements.

SolutionBest ForLearning CurveBundle Size
Context APISmall apps, theme/authLow0kb (built-in)
ZustandMedium apps, simple global stateLow~1kb
Redux ToolkitLarge apps, complex stateMedium-High~12kb
Jotai/RecoilAtomic state needsMedium~3-5kb

Context API: When Simple is Enough

Context is perfect for infrequently changing data like theme, authentication, or user preferences. However, it causes re-renders of all consumers when any value changes.

typescript
// Good use case: Auth context
interface AuthContextType {
  user: User | null;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  
  const login = useCallback(async (credentials: Credentials) => {
    const user = await authApi.login(credentials);
    setUser(user);
  }, []);
  
  const logout = useCallback(() => {
    setUser(null);
    authApi.logout();
  }, []);
  
  const value = useMemo(
    () => ({ user, login, logout }),
    [user, login, logout]
  );
  
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Zustand: The Sweet Spot

Zustand offers a minimal API with excellent performance. It's my go-to for most React Native apps. No providers, no boilerplate, just clean state management.

typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set) => ({
      items: [],
      addItem: (item) =>
        set((state) => ({
          items: [...state.items, item],
        })),
      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((item) => item.id !== id),
        })),
      clearCart: () => set({ items: [] }),
    }),
    {
      name: 'cart-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// Usage in component
function CartScreen() {
  const items = useCartStore((state) => state.items);
  const removeItem = useCartStore((state) => state.removeItem);
  // Component only re-renders when items change
}

Redux Toolkit: For Complex Apps

Redux shines in large apps with complex state interactions, time-travel debugging needs, or teams already familiar with it. Modern Redux Toolkit eliminates most boilerplate.

typescript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

interface ProductsState {
  items: Product[];
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
}

export const fetchProducts = createAsyncThunk(
  'products/fetchProducts',
  async (category: string) => {
    const response = await api.getProducts(category);
    return response.data;
  }
);

const productsSlice = createSlice({
  name: 'products',
  initialState: {
    items: [],
    status: 'idle',
    error: null,
  } as ProductsState,
  reducers: {
    productUpdated: (state, action: PayloadAction<Product>) => {
      const index = state.items.findIndex(p => p.id === action.payload.id);
      if (index !== -1) {
        state.items[index] = action.payload;
      }
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message || null;
      });
  },
});
State Management Solutions Comparison Chart

State Management Solutions Comparison Chart

Decision Framework

Start with Context for auth/theme. Graduate to Zustand when you need more. Only reach for Redux when you have truly complex state requirements, multiple teams, or need advanced debugging. Don't over-engineer—you can always migrate later.