Skip to main content

Skillber v1.0 is here!

Learn more

Context & Global State

Checking access...

As your app grows, you’ll find yourself passing props through many levels of components — a problem called prop drilling. React Context provides a way to share values between components without explicitly passing props through every level.

The Problem: Prop Drilling

function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return (
<div>
<Header user={user} setUser={setUser} />
<Sidebar user={user} />
<Content user={user} />
</div>
);
}
function Header({ user, setUser }) {
return (
<nav>
<UserMenu user={user} onLogout={() => setUser(null)} />
</nav>
);
}

The user prop passes through Layout — which doesn’t need it — just to reach Header and UserMenu.

createContext and useContext

import { createContext, useContext } from "react";
// 1. Create the context
const ThemeContext = createContext(null);
// 2. Provide the value
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Consume the value anywhere in the tree
function Navbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div className={`navbar navbar-${theme}`}>
<span>Current theme: {theme}</span>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
</div>
);
}

Any component inside <ThemeContext.Provider> can access the context value — no props needed.

Creating a Context Module

Organise contexts in their own files:

contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session on mount
checkSession()
.then(setUser)
.finally(() => setLoading(false));
}, []);
const login = async (email, password) => {
const user = await authAPI.login(email, password);
setUser(user);
};
const logout = async () => {
await authAPI.logout();
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

Usage anywhere in the app:

import { useAuth } from "./contexts/AuthContext";
function Profile() {
const { user, logout } = useAuth();
if (!user) return <p>Not logged in</p>;
return (
<div>
<h2>{user.name}</h2>
<button onClick={logout}>Log Out</button>
</div>
);
}

Context + useReducer

For complex state, combine Context with useReducer instead of useState:

contexts/CartContext.jsx
import { createContext, useContext, useReducer } from "react";
const CartContext = createContext(null);
const initialState = {
items: [],
total: 0,
};
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM": {
const existing = state.items.find((i) => i.id === action.item.id);
const items = existing
? state.items.map((i) =>
i.id === action.item.id
? { ...i, quantity: i.quantity + 1 }
: i
)
: [...state.items, { ...action.item, quantity: 1 }];
return { ...state, items, total: calculateTotal(items) };
}
case "REMOVE_ITEM":
const items = state.items.filter((i) => i.id !== action.id);
return { ...state, items, total: calculateTotal(items) };
case "UPDATE_QUANTITY":
const updated = state.items.map((i) =>
i.id === action.id ? { ...i, quantity: Math.max(0, action.quantity) } : i
).filter((i) => i.quantity > 0);
return { ...state, items: updated, total: calculateTotal(updated) };
case "CLEAR_CART":
return initialState;
default:
return state;
}
}
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, initialState);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error("useCart must be used within a CartProvider");
}
return context;
}

Usage:

import { useCart } from "./contexts/CartContext";
function ProductCard({ product }) {
const { dispatch } = useCart();
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => dispatch({ type: "ADD_ITEM", item: product })}>
Add to Cart
</button>
</div>
);
}
function CartSummary() {
const { cart } = useCart();
return (
<div>
<h2>Cart ({cart.items.length} items)</h2>
<p>Total: ${cart.total.toFixed(2)}</p>
{cart.items.map((item) => (
<CartItem key={item.id} item={item} />
))}
</div>
);
}
function CartItem({ item }) {
const { dispatch } = useCart();
return (
<div>
<span>{item.name}</span>
<button onClick={() => dispatch({ type: "REMOVE_ITEM", id: item.id })}>
</button>
</div>
);
}

Multiple Contexts

You can use multiple providers, nested:

function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}

Each context is independent. A component can consume any or all of them:

function UserDashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { cart } = useCart();
// ...
}

Context Performance Caveats

When context value changes, all consumers re-render. For frequently changing values, consider:

1. Split contexts by concern

// ❌ One context for everything — any change re-renders all consumers
<AppContext.Provider value={{ user, theme, cart, notifications }} />
// ✅ Split into separate contexts
<AuthProvider>
<ThemeProvider>
<CartProvider>
<NotificationProvider>
<App />
</NotificationProvider>
</CartProvider>
</ThemeProvider>
</AuthProvider>

2. Memoize context values

function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
// Without useMemo: new object every render → all consumers re-render
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

When to Use Context vs Props vs State

PatternWhen to UseExample
PropsPassing data to direct children<Button label="Click" />
Lifting stateSiblings share stateParent manages shared state
ContextDeep tree sharingTheme, auth, locale, cart
State manager (Redux/Zustand)Complex global state, many updatesLarge app with complex interactions

Quick Reference

// Create context
const MyContext = createContext(defaultValue);
// Provide value
<MyContext.Provider value={someValue}>
<Children />
</MyContext.Provider>
// Consume value
const value = useContext(MyContext);
// Custom hook pattern
export function useMyContext() {
const ctx = useContext(MyContext);
if (!ctx) throw new Error("useMyContext must be used within MyProvider");
return ctx;
}
// Context + reducer
const [state, dispatch] = useReducer(reducer, initialState);
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>

Practice Exercises

  1. Theme switcher with Context: Create a ThemeContext with theme and toggleTheme. Provide it at the app level. Consume it in a Navbar, a Card, and a Button component. All three should respond to theme changes.

  2. Authentication context with reducer: Build an AuthContext that uses useReducer with actions: LOGIN, LOGOUT, SET_LOADING. Create a custom useAuth hook. Use it in a protected route setup.

  3. Shopping cart with context + reducer: Build a full cart system with context and reducer. Include actions: ADD_ITEM, REMOVE_ITEM, UPDATE_QUANTITY, CLEAR_CART. Create a cart icon in the header showing item count.