
Managing state is one of the most important parts of building scalable React Native apps. With so many options available in 2025, how do you know which React Native state management solution to choose? The wrong choice can lead to performance issues, complex debugging, and technical debt. The right choice enables clean architecture, predictable behavior, and easy testing.
In this post, we’ll compare three of the most popular state management tools in the React Native ecosystem: Context API, Redux, and Zustand. We’ll explore when to use each with comprehensive code examples, performance considerations, and real-world patterns.
Why State Management Matters
State refers to data that determines how your app behaves and looks—like logged-in users, theme settings, cart items, or form inputs. Without organized state management, your app becomes hard to maintain as it grows. Common symptoms of poor state management include:
- Prop drilling through many component levels
- Inconsistent data across screens
- Difficult debugging when state changes unexpectedly
- Performance issues from unnecessary re-renders
- Race conditions with async operations
1. React Context API
The Context API is built into React and allows you to share state across your component tree without prop drilling. It’s the simplest solution but requires careful optimization for performance.
Complete Context API Implementation
// contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
}
interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check for existing session on mount
useEffect(() => {
const checkAuth = async () => {
try {
const token = await AsyncStorage.getItem('authToken');
if (token) {
// Validate token with API
const response = await fetch('https://api.example.com/auth/me', {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
await AsyncStorage.removeItem('authToken');
}
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
const login = useCallback(async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const { user, token } = await response.json();
await AsyncStorage.setItem('authToken', token);
setUser(user);
} finally {
setIsLoading(false);
}
}, []);
const logout = useCallback(async () => {
try {
await AsyncStorage.removeItem('authToken');
setUser(null);
} catch (error) {
console.error('Logout failed:', error);
}
}, []);
const register = useCallback(async (email: string, password: string, name: string) => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Registration failed');
}
const { user, token } = await response.json();
await AsyncStorage.setItem('authToken', token);
setUser(user);
} finally {
setIsLoading(false);
}
}, []);
// Memoize context value to prevent unnecessary re-renders
const value = useMemo(() => ({
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
register,
}), [user, isLoading, login, logout, register]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Optimized selectors to prevent re-renders
export function useIsAuthenticated() {
const { isAuthenticated } = useAuth();
return isAuthenticated;
}
export function useUser() {
const { user } = useAuth();
return user;
}
// contexts/ThemeContext.tsx
import React, { createContext, useContext, useState, useMemo, useCallback, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
type ThemeMode = 'light' | 'dark' | 'system';
interface Theme {
colors: {
background: string;
surface: string;
text: string;
textSecondary: string;
primary: string;
error: string;
border: string;
};
spacing: {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
};
}
const lightTheme: Theme = {
colors: {
background: '#FFFFFF',
surface: '#F5F5F5',
text: '#000000',
textSecondary: '#666666',
primary: '#007AFF',
error: '#FF3B30',
border: '#E0E0E0',
},
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
};
const darkTheme: Theme = {
colors: {
background: '#000000',
surface: '#1C1C1E',
text: '#FFFFFF',
textSecondary: '#8E8E93',
primary: '#0A84FF',
error: '#FF453A',
border: '#38383A',
},
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
};
interface ThemeContextType {
theme: Theme;
isDark: boolean;
themeMode: ThemeMode;
setThemeMode: (mode: ThemeMode) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const systemColorScheme = useColorScheme();
const [themeMode, setThemeModeState] = useState<ThemeMode>('system');
// Load saved preference
useEffect(() => {
AsyncStorage.getItem('themeMode').then((saved) => {
if (saved) setThemeModeState(saved as ThemeMode);
});
}, []);
const setThemeMode = useCallback((mode: ThemeMode) => {
setThemeModeState(mode);
AsyncStorage.setItem('themeMode', mode);
}, []);
const isDark = useMemo(() => {
if (themeMode === 'system') {
return systemColorScheme === 'dark';
}
return themeMode === 'dark';
}, [themeMode, systemColorScheme]);
const theme = isDark ? darkTheme : lightTheme;
const value = useMemo(() => ({
theme,
isDark,
themeMode,
setThemeMode,
}), [theme, isDark, themeMode, setThemeMode]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Context API Pros
- Built-in—no need for external libraries
- Simple to set up for basic use cases
- Perfect for infrequently changing state (auth, theme)
- Zero bundle size impact
Context API Cons
- Not optimized for frequent updates (can trigger full re-renders)
- Lacks built-in dev tools and middleware
- Manual optimization required (useMemo, useCallback)
- Can become unwieldy with many contexts
2. Redux with Redux Toolkit
Redux is a time-tested state management library that offers predictable state changes via actions and reducers. Redux Toolkit (RTK) eliminates most boilerplate and is the recommended way to use Redux in 2025.
Complete Redux Toolkit Implementation
// store/index.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import authReducer from './slices/authSlice';
import cartReducer from './slices/cartSlice';
import productsReducer from './slices/productsSlice';
import { api } from './api';
const rootReducer = combineReducers({
auth: authReducer,
cart: cartReducer,
products: productsReducer,
[api.reducerPath]: api.reducer,
});
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['auth', 'cart'], // Only persist these slices
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(api.middleware),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
token: null,
isLoading: false,
error: null,
};
export const login = createAsyncThunk(
'auth/login',
async ({ email, password }: { email: string; password: string }, { rejectWithValue }) => {
try {
const response = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
return rejectWithValue(error.message || 'Login failed');
}
return response.json();
} catch (error) {
return rejectWithValue('Network error');
}
}
);
export const logout = createAsyncThunk('auth/logout', async () => {
// Perform any cleanup
return null;
});
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
setToken: (state, action: PayloadAction<string>) => {
state.token = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
.addCase(logout.fulfilled, (state) => {
state.user = null;
state.token = null;
});
},
});
export const { clearError, setToken } = authSlice.actions;
export default authSlice.reducer;
// Selectors
export const selectUser = (state: RootState) => state.auth.user;
export const selectIsAuthenticated = (state: RootState) => !!state.auth.token;
export const selectAuthLoading = (state: RootState) => state.auth.isLoading;
export const selectAuthError = (state: RootState) => state.auth.error;
// store/slices/cartSlice.ts
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { RootState } from '..';
interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
imageUrl: string;
}
interface CartState {
items: CartItem[];
}
const initialState: CartState = {
items: [],
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addToCart: (state, action: PayloadAction<Omit<CartItem, 'quantity'> & { quantity?: number }>) => {
const existingIndex = state.items.findIndex(
item => item.productId === action.payload.productId
);
if (existingIndex >= 0) {
state.items[existingIndex].quantity += action.payload.quantity ?? 1;
} else {
state.items.push({
...action.payload,
quantity: action.payload.quantity ?? 1,
});
}
},
removeFromCart: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(item => item.productId !== action.payload);
},
updateQuantity: (state, action: PayloadAction<{ productId: string; quantity: number }>) => {
const item = state.items.find(item => item.productId === action.payload.productId);
if (item) {
if (action.payload.quantity <= 0) {
state.items = state.items.filter(i => i.productId !== action.payload.productId);
} else {
item.quantity = action.payload.quantity;
}
}
},
clearCart: (state) => {
state.items = [];
},
},
});
export const { addToCart, removeFromCart, updateQuantity, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// Memoized selectors
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((total, item) => total + item.price * item.quantity, 0)
);
export const selectCartItemCount = createSelector(
[selectCartItems],
(items) => items.reduce((count, item) => count + item.quantity, 0)
);
export const selectIsInCart = (productId: string) => createSelector(
[selectCartItems],
(items) => items.some(item => item.productId === productId)
);
// Usage in component
import React from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { useAppSelector, useAppDispatch } from '../store';
import { selectCartItems, selectCartTotal, removeFromCart, updateQuantity } from '../store/slices/cartSlice';
export const CartScreen: React.FC = () => {
const dispatch = useAppDispatch();
const items = useAppSelector(selectCartItems);
const total = useAppSelector(selectCartTotal);
const handleRemove = (productId: string) => {
dispatch(removeFromCart(productId));
};
const handleUpdateQuantity = (productId: string, quantity: number) => {
dispatch(updateQuantity({ productId, quantity }));
};
return (
<View style={styles.container}>
<FlatList
data={items}
keyExtractor={(item) => item.productId}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item.name}</Text>
<Text>${item.price} x {item.quantity}</Text>
<TouchableOpacity onPress={() => handleRemove(item.productId)}>
<Text>Remove</Text>
</TouchableOpacity>
</View>
)}
/>
<Text style={styles.total}>Total: ${total.toFixed(2)}</Text>
</View>
);
};
Redux Pros
- Centralized and predictable state
- Excellent DevTools for debugging
- Strong ecosystem (RTK Query, middleware, persistence)
- Great for large teams with clear patterns
- Time-travel debugging
Redux Cons
- More setup than simpler solutions
- Requires understanding of Redux patterns
- Can be overkill for simple apps
3. Zustand
Zustand is a lightweight, fast, and intuitive state management solution. It’s rapidly gaining popularity because it offers Redux-like power with minimal boilerplate.
Complete Zustand Implementation
// stores/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;
}
interface AuthStore {
user: User | null;
token: string | null;
isLoading: boolean;
error: string | null;
// Actions
login: (email: string, password: string) => Promise<void>;
logout: () => void;
clearError: () => void;
setUser: (user: User) => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
user: null,
token: null,
isLoading: false,
error: null,
login: async (email: string, password: string) => {
set({ isLoading: true, error: null });
try {
const response = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const { user, token } = await response.json();
set({ user, token, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Login failed',
isLoading: false
});
}
},
logout: () => {
set({ user: null, token: null });
},
clearError: () => set({ error: null }),
setUser: (user: User) => set({ user }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ user: state.user, token: state.token }),
}
)
);
// Selectors (for optimized re-renders)
export const useUser = () => useAuthStore((state) => state.user);
export const useIsAuthenticated = () => useAuthStore((state) => !!state.token);
export const useAuthLoading = () => useAuthStore((state) => state.isLoading);
export const useAuthError = () => useAuthStore((state) => state.error);
export const useAuthActions = () => useAuthStore((state) => ({
login: state.login,
logout: state.logout,
clearError: state.clearError,
}));
// stores/cartStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
imageUrl: string;
}
interface CartStore {
items: CartItem[];
// Computed
total: () => number;
itemCount: () => number;
isInCart: (productId: string) => boolean;
getQuantity: (productId: string) => number;
// Actions
addToCart: (item: Omit<CartItem, 'quantity'>, quantity?: number) => void;
removeFromCart: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
}
export const useCartStore = create<CartStore>()(
persist(
immer((set, get) => ({
items: [],
// Computed values
total: () => {
return get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
itemCount: () => {
return get().items.reduce((count, item) => count + item.quantity, 0);
},
isInCart: (productId: string) => {
return get().items.some(item => item.productId === productId);
},
getQuantity: (productId: string) => {
return get().items.find(item => item.productId === productId)?.quantity ?? 0;
},
// Actions with Immer for immutable updates
addToCart: (item, quantity = 1) => {
set((state) => {
const existingIndex = state.items.findIndex(
i => i.productId === item.productId
);
if (existingIndex >= 0) {
state.items[existingIndex].quantity += quantity;
} else {
state.items.push({ ...item, quantity });
}
});
},
removeFromCart: (productId) => {
set((state) => {
state.items = state.items.filter(item => item.productId !== productId);
});
},
updateQuantity: (productId, quantity) => {
set((state) => {
if (quantity <= 0) {
state.items = state.items.filter(item => item.productId !== productId);
} else {
const item = state.items.find(i => i.productId === productId);
if (item) item.quantity = quantity;
}
});
},
clearCart: () => set({ items: [] }),
})),
{
name: 'cart-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// stores/productStore.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
category: string;
}
interface ProductStore {
products: Product[];
isLoading: boolean;
error: string | null;
selectedCategory: string | null;
searchQuery: string;
// Computed
filteredProducts: () => Product[];
categories: () => string[];
// Actions
fetchProducts: () => Promise<void>;
setCategory: (category: string | null) => void;
setSearchQuery: (query: string) => void;
}
export const useProductStore = create<ProductStore>()(
devtools(
(set, get) => ({
products: [],
isLoading: false,
error: null,
selectedCategory: null,
searchQuery: '',
filteredProducts: () => {
const { products, selectedCategory, searchQuery } = get();
return products.filter(product => {
const matchesCategory = !selectedCategory || product.category === selectedCategory;
const matchesSearch = !searchQuery ||
product.name.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
},
categories: () => {
const { products } = get();
return [...new Set(products.map(p => p.category))];
},
fetchProducts: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error('Failed to fetch products');
const products = await response.json();
set({ products, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to load products',
isLoading: false
});
}
},
setCategory: (category) => set({ selectedCategory: category }),
setSearchQuery: (query) => set({ searchQuery: query }),
}),
{ name: 'product-store' }
)
);
// Usage in component - no Provider needed!
import React, { useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { useCartStore } from '../stores/cartStore';
import { useProductStore } from '../stores/productStore';
export const ProductListScreen: React.FC = () => {
const { fetchProducts, filteredProducts, isLoading, error } = useProductStore();
const addToCart = useCartStore(state => state.addToCart);
const isInCart = useCartStore(state => state.isInCart);
const cartItemCount = useCartStore(state => state.itemCount());
useEffect(() => {
fetchProducts();
}, []);
const products = filteredProducts();
return (
<View style={styles.container}>
<Text>Cart: {cartItemCount} items</Text>
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.product}>
<Text>{item.name} - ${item.price}</Text>
<TouchableOpacity
onPress={() => addToCart(item)}
disabled={isInCart(item.id)}
>
<Text>{isInCart(item.id) ? 'In Cart' : 'Add to Cart'}</Text>
</TouchableOpacity>
</View>
)}
/>
</View>
);
};
Zustand Pros
- Minimal setup, zero boilerplate
- No Provider needed—just import and use
- Built-in support for persist, middleware, devtools
- Selective re-renders with subscriptions
- TypeScript-first design
- Tiny bundle size (~1kb)
Zustand Cons
- Smaller community compared to Redux
- Less familiar in enterprise teams
- Fewer pre-built integrations
Comparison Table
| Feature | Context API | Redux Toolkit | Zustand |
|---|---|---|---|
| Boilerplate | Low | Medium | Very Low |
| Performance | Medium | High | High |
| Bundle Size | 0kb | ~10kb | ~1kb |
| DevTools | None | Excellent | Good |
| Middleware | Manual | Built-in | Built-in |
| Learning Curve | Low | Medium | Low |
| Persistence | Manual | redux-persist | Built-in |
| Large Apps | Not ideal | Excellent | Good |
Common Mistakes to Avoid
Putting Everything in Global State
Not all state needs to be global. Form input values, UI toggles, and component-specific state should remain local with useState.
Not Memoizing Selectors
Derived values should be memoized with createSelector (Redux) or computed functions (Zustand) to prevent unnecessary recalculations.
Mixing State Management Solutions
Pick one primary solution for global state. Using Context + Redux + Zustand together creates confusion. Context for theme/auth + one library for app state is acceptable.
Ignoring TypeScript
All three solutions have excellent TypeScript support. Skipping types leads to runtime errors that could be caught at compile time.
Which One Should You Use?
- Use Context API for simple global states (themes, auth, language preferences)
- Use Redux Toolkit when your app needs structure, excellent debugging, and your team knows Redux
- Use Zustand if you want lightweight, modern, and scalable state management with minimal boilerplate
Final Thoughts
Your choice should depend on your app size, team preferences, and long-term maintainability. Each of these tools solves state in its own way—the real key is consistency and clean architecture. For most new React Native projects in 2025, I recommend starting with Zustand and only moving to Redux if you need its specific features.
For architecture patterns that work well with any state solution, check out our guide on MVVM Architecture in React Native. For the essential libraries to pair with your state management choice, see React Native Libraries for 2025. And for official documentation, visit Zustand’s documentation.
1 Comment