State Management in React Native
React Native uses the same state management patterns as React web. You can use
useState, useReducer, Context API, or external libraries
like Redux, Zustand, or React Query.
Local State with useState
import { useState } from 'react';
import { View, Text, TextInput, Button, FlatList } from 'react-native';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([
...todos,
{ id: Date.now().toString(), text: input, completed: false }
]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<View style={styles.container}>
<TextInput
value={input}
onChangeText={setInput}
placeholder="Add a todo..."
style={styles.input}
/>
<Button title="Add" onPress={addTodo} />
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TodoItem
todo={item}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
)}
/>
</View>
);
}
Context API for Global State
// context/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
type User = { id: string; name: string; email: string };
type AuthContextType = {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await api.login(email, password);
setUser(response.user);
await AsyncStorage.setItem('token', response.token);
} finally {
setIsLoading(false);
}
};
const logout = async () => {
setUser(null);
await AsyncStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for using auth
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage in App.tsx
function App() {
return (
<AuthProvider>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</AuthProvider>
);
}
// Usage in components
function ProfileScreen() {
const { user, logout } = useAuth();
return (
<View>
<Text>Welcome, {user?.name}!</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}
Data Fetching with React Query
// Install: npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';
const queryClient = new QueryClient();
// Wrap your app
function App() {
return (
<QueryClientProvider client={queryClient}>
<MainApp />
</QueryClientProvider>
);
}
// Fetching data
function UsersList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('https://api.example.com/users');
return response.json();
},
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
});
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <UserCard user={item} />}
refreshing={isLoading}
onRefresh={refetch}
/>
);
}
// Mutations (POST, PUT, DELETE)
function CreateUser() {
const mutation = useMutation({
mutationFn: async (newUser) => {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<Button
title="Create User"
onPress={() => mutation.mutate({ name: 'John' })}
disabled={mutation.isPending}
/>
);
}
AsyncStorage - Persistent Storage
// Install: npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
// Store data
const storeData = async (key: string, value: any) => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
} catch (e) {
console.error('Error storing data:', e);
}
};
// Retrieve data
const getData = async (key: string) => {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.error('Error reading data:', e);
return null;
}
};
// Remove data
const removeData = async (key: string) => {
try {
await AsyncStorage.removeItem(key);
} catch (e) {
console.error('Error removing data:', e);
}
};
// Custom hook for persistent state
function usePersistentState<T>(key: string, defaultValue: T) {
const [state, setState] = useState<T>(defaultValue);
const [isLoading, setIsLoading] = useState(true);
// Load on mount
useEffect(() => {
getData(key).then((value) => {
if (value !== null) setState(value);
setIsLoading(false);
});
}, [key]);
// Save on change
const setPersistentState = async (value: T) => {
setState(value);
await storeData(key, value);
};
return [state, setPersistentState, isLoading] as const;
}
// Usage
function Settings() {
const [theme, setTheme, loading] = usePersistentState('theme', 'light');
if (loading) return <ActivityIndicator />;
return (
<Switch
value={theme === 'dark'}
onValueChange={(value) => setTheme(value ? 'dark' : 'light')}
/>
);
}
Zustand - Simple State Management
// Install: npm install zustand
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Simple store
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// With persistence
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
}
const useCartStore = create<CartStore>()(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id)
})),
clearCart: () => set({ items: [] }),
}),
{
name: 'cart-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// Usage in components
function CartButton() {
const itemCount = useCartStore((state) => state.items.length);
const clearCart = useCartStore((state) => state.clearCart);
return (
<View>
<Text>Cart: {itemCount} items</Text>
<Button title="Clear" onPress={clearCart} />
</View>
);
}
📊 When to Use What
- • useState: Local component state
- • Context: Theme, auth, app-wide settings
- • React Query: Server state, API data, caching
- • Zustand/Redux: Complex client state, shopping carts
- • AsyncStorage: Persistent data (tokens, preferences)
API Service Pattern
// services/api.ts
const BASE_URL = 'https://api.example.com';
class ApiClient {
private token: string | null = null;
setToken(token: string | null) {
this.token = token;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
...options.headers,
};
const response = await fetch(`${BASE_URL}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// GET request
get<T>(endpoint: string) {
return this.request<T>(endpoint);
}
// POST request
post<T>(endpoint: string, data: any) {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
// PUT request
put<T>(endpoint: string, data: any) {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
// DELETE request
delete<T>(endpoint: string) {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
export const api = new ApiClient();
// Usage
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });