Chapter 4: useContext
The Problem: Prop Drilling
When data needs to pass through multiple levels of components:
function App() {
const [user, setUser] = useState({ name: 'Jean' });
return <Layout user={user} />;
}
function Layout({ user }) {
return (
<div>
<Header user={user} />
<Main user={user} />
</div>
);
}
function Header({ user }) {
return <Navbar user={user} />;
}
function Navbar({ user }) {
return <UserMenu user={user} />; // Finally used here!
}
function UserMenu({ user }) {
return <span>{user.name}</span>;
}
Layout and Header only forward user without using it. This is prop drilling.
The Solution: Context
Context allows you to "teleport" data through the component tree.
Step 1: Create the Context
import { createContext } from 'react';
const UserContext = createContext(null);
Step 2: Provide the Value (Provider)
function App() {
const [user, setUser] = useState({ name: 'Jean' });
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
Step 3: Consume the Value (useContext)
import { useContext } from 'react';
function UserMenu() {
const user = useContext(UserContext);
return <span>{user.name}</span>;
}
Intermediate components no longer need to pass user:
function Layout() {
return (
<div>
<Header />
<Main />
</div>
);
}
function Header() {
return <Navbar />;
}
function Navbar() {
return <UserMenu />;
}
Complete Example: Theme
// ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// App.js
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
// ThemeToggle.js
import { useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current mode: {theme}
</button>
);
}
// Card.js
import { useTheme } from './ThemeContext';
function Card({ children }) {
const { theme } = useTheme();
return (
<div className={`card card-${theme}`}>
{children}
</div>
);
}
Passing Functions via Context
You can pass functions to allow children to modify the state:
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const response = await api.login(credentials);
setUser(response.user);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
return useContext(AuthContext);
}
// Usage
function LoginButton() {
const { user, login, logout } = useAuth();
if (user) {
return <button onClick={logout}>Logout</button>;
}
return <button onClick={() => login({ email, password })}>Login</button>;
}
When to Use Context?
Good use cases:
- Theme (dark/light mode)
- Logged in user
- Language / internationalization
- Global configuration
Bad use cases:
- Local form state
- Data that concerns only a few nearby components
- State that changes very frequently (performance)
Context vs Props
| Criteria | Props | Context |
|---|---|---|
| Data concerns few components | ✅ | ❌ |
| Data passes through many levels | ❌ | ✅ |
| Explicit data flow | ✅ | ❌ |
| Global configuration | ❌ | ✅ |
Multiple Contexts
You can nest multiple Providers:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<Layout />
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
Exercises
Exercise 1: Language Context
Create a context to manage language (fr/en) with a button to change it.
Solution:
// LanguageContext.js
import { createContext, useContext, useState } from 'react';
const LanguageContext = createContext(null);
const translations = {
fr: { greeting: 'Bonjour', button: 'English' },
en: { greeting: 'Hello', button: 'French' }
};
export function LanguageProvider({ children }) {
const [lang, setLang] = useState('fr');
const toggleLang = () => {
setLang(prev => prev === 'fr' ? 'en' : 'fr');
};
const t = (key) => translations[lang][key];
return (
<LanguageContext.Provider value={{ lang, toggleLang, t }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
return useContext(LanguageContext);
}
// Greeting.js
function Greeting() {
const { t, toggleLang } = useLanguage();
return (
<div>
<h1>{t('greeting')}</h1>
<button onClick={toggleLang}>{t('button')}</button>
</div>
);
}
Key Points to Remember
- Context avoids prop drilling
- createContext -> Provider -> useContext
- Create a custom hook (useTheme, useAuth) to simplify usage
- Use for truly global data
- Don't overuse: props are often clearer
Module 3 Summary
| Hook | Usage | Triggers Re-render |
|---|---|---|
| useState | Local state | Yes |
| useEffect | Side effects (API, events) | No (but can modify state) |
| useRef | DOM and persistent values | No |
| useContext | Global data | Yes (if value changes) |
<- Previous Chapter | Back to module | Next Module: Advanced React ->