React Native Context API State Management
As your app grows, passing data through props across many layers becomes painful. Context API solves this by making data available to any component in the tree without threading props manually through every level.
The Prop Drilling Problem
Without Context — "Prop Drilling":
App (holds user data)
└── Layout (passes user down)
└── Header (passes user down)
└── AvatarButton (finally uses user!)
user must be passed at every level even if Layout and Header don't use it.
App
│ user={user}
▼
Layout
│ user={user} ← doesn't need it, just passes it on
▼
Header
│ user={user} ← doesn't need it, just passes it on
▼
AvatarButton ← finally uses user.avatar
With Context — Any component reads it directly:
App (provides user via Context)
└── Layout
└── Header
└── AvatarButton ──reads──► Context (gets user directly)
Three Steps to Use Context
Step 1: CREATE the context Step 2: PROVIDE the context (wrap components with a Provider) Step 3: CONSUME the context (read it in any child component)
Step 1 — Create the Context
// context/AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
// null is the default value — used when no Provider is above in the tree
Step 2 — Create the Provider
The Provider wraps parts of your app and makes the context value available to all descendants.
// context/AuthContext.js (continued)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
function login(userData, authToken) {
setUser(userData);
setToken(authToken);
}
function logout() {
setUser(null);
setToken(null);
}
return (
<AuthContext.Provider value={{ user, token, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
Step 3 — Wrap Your App with the Provider
// App.js
import { AuthProvider } from './context/AuthContext';
export default function App() {
return (
<AuthProvider>
<NavigationContainer>
{/* All screens inside here can access auth context */}
</NavigationContainer>
</AuthProvider>
);
}
Step 4 — Consume in Any Component
// screens/ProfileScreen.js
import { useAuth } from '../context/AuthContext';
export default function ProfileScreen() {
const { user, logout } = useAuth();
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 20 }}>Welcome, {user?.name}</Text>
<Text style={{ color: '#666' }}>{user?.email}</Text>
<TouchableOpacity onPress={logout}>
<Text>Logout</Text>
</TouchableOpacity>
</View>
);
}
// screens/HeaderAvatar.js — deeply nested, still works
import { useAuth } from '../context/AuthContext';
function HeaderAvatar() {
const { user } = useAuth(); // reads directly — no prop needed
return (
<Image
source={{ uri: user?.avatar }}
style={{ width: 36, height: 36, borderRadius: 18 }}
/>
);
}
Building a Theme Context
A theme context lets every component know whether dark mode is on, and provides the right colors automatically.
// context/ThemeContext.js
import { createContext, useContext, useState } from 'react';
const light = {
background: '#ffffff',
text: '#111111',
card: '#f5f5f5',
primary: '#007AFF',
};
const dark = {
background: '#000000',
text: '#ffffff',
card: '#1c1c1e',
primary: '#0A84FF',
};
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
const colors = isDark ? dark : light;
return (
<ThemeContext.Provider value={{ colors, isDark, setIsDark }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Any screen uses colors without knowing isDark or the full theme object
function HomeScreen() {
const { colors } = useTheme();
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<Text style={{ color: colors.text }}>Home Screen</Text>
</View>
);
}
Theme switch diagram:
isDark = false isDark = true
┌────────────────────┐ ┌────────────────────┐
│ bg: white │ │ bg: black │
│ text: #111 │ ───► │ text: white │
│ card: #f5f5f5 │ │ card: #1c1c1e │
└────────────────────┘ └────────────────────┘
All screens update instantly
Multiple Contexts Together
export default function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<NavigationContainer>
{/* All three contexts available everywhere */}
</NavigationContainer>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}
When Context Re-renders
When the context value changes, every component that consumes it re-renders. Keep context values focused. Split a large context into smaller ones so a color preference change does not force the user list to re-render.
Too broad (causes unnecessary re-renders):
<AppContext.Provider value={{ user, theme, cart, posts, settings }}>
Better (each context updates independently):
<AuthContext.Provider value={{ user }}>
<ThemeContext.Provider value={{ theme }}>
<CartContext.Provider value={{ cart }}>
Context vs Redux
Context API Redux ──────────────────────────────────────────────────────────── Built into React — no install Separate library (redux toolkit) Great for simple global state Better for complex state logic Easy to learn More setup, more powerful Good for: theme, auth, language Good for: large apps, many actions
Start with Context API. Add Redux or Zustand later only when your state management becomes genuinely complex.
Summary
Context API eliminates prop drilling by making data available to any component in the tree. Create a context file with a Provider component and a custom hook. Wrap your app in the Provider. Call the custom hook in any component that needs the data. Keep each context focused on one concern — auth, theme, or cart — to avoid unnecessary re-renders.
