Passing data through props works great for simple apps, but what happens when you need to share state between components that are many levels apart? You end up passing props through every intermediate component - this is called prop drilling and it's tedious and error-prone. The useContext hook solves this by letting you share data across your entire component tree without passing props. Let's learn how to use Context effectively! š³
The Problem: Prop Drilling
Imagine you have user data at the top level that needs to be accessed deep in the tree:
// ā Problem: Prop drilling
function App() {
const [user, setUser] = React.useState({ name: 'Alice', theme: 'dark' })
return <Dashboard user={user} />
}
function Dashboard({ user }) {
// Dashboard doesn't use user, just passes it down
return <Sidebar user={user} />
}
function Sidebar({ user }) {
// Sidebar doesn't use user, just passes it down
return <Profile user={user} />
}
function Profile({ user }) {
// Finally! Profile uses user
return <div>Welcome, {user.name}!</div>
}
// Problems:
// 1. Dashboard and Sidebar don't need user, but must pass it
// 2. If user prop name changes, must update ALL components
// 3. Hard to maintain as app grows
// 4. Intermediate components are tightly coupledWhat is Prop Drilling?
Prop drilling is when you pass data through multiple levels of components, even though intermediate components don't use the data.
It's like passing a package through many people to reach the final recipient - inefficient and annoying!
The Solution: Context
Context lets components access data without passing props:
// ā
Solution: Use Context
// Step 1: Create Context
const UserContext = React.createContext()
// Step 2: Provide Context
function App() {
const [user, setUser] = React.useState({ name: 'Alice', theme: 'dark' })
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
)
}
// Step 3: Components in the middle don't need to know about user
function Dashboard() {
return <Sidebar />
}
function Sidebar() {
return <Profile />
}
// Step 4: Consume Context where needed
function Profile() {
const user = React.useContext(UserContext)
return <div>Welcome, {user.name}!</div>
}
// Benefits:
// 1. Dashboard and Sidebar are simpler
// 2. Profile directly accesses user
// 3. Adding new consumer is easy
// 4. No prop drilling!Three Steps to Use Context
Step 1: Create Context
// Create context outside component
const ThemeContext = React.createContext()
// Can provide default value (optional)
const ThemeContext = React.createContext('light')
// Common pattern: Create in separate file
// theme-context.js
import React from 'react'
export const ThemeContext = React.createContext('light')
// Note: Default value is used only if component has no Provider above itStep 2: Provide Context
function App() {
const [theme, setTheme] = React.useState('light')
return (
<ThemeContext.Provider value={theme}>
{/* All children can access theme */}
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
)
}
// Provider makes value available to all descendants
// Value can be anything: string, number, object, array, function
// Passing object with state and setter:
function App() {
const [theme, setTheme] = React.useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
)
}Step 3: Consume Context
function Header() {
// Use useContext hook to access value
const theme = React.useContext(ThemeContext)
return (
<header className={theme}>
<h1>My App</h1>
</header>
)
}
// If context provides object:
function ThemeToggle() {
const { theme, setTheme } = React.useContext(ThemeContext)
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
)
}Complete Context Example
Theme Context (Full Implementation)
// 1. Create Context
const ThemeContext = React.createContext()
// 2. Create Provider Component
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
const value = {
theme,
toggleTheme
}
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// 3. Create Custom Hook (optional but recommended)
function useTheme() {
const context = React.useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// 4. Use in App
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
)
}
// 5. Consume in Components
function Header() {
const { theme } = useTheme()
return (
<header className={theme}>
<h1>My App</h1>
</header>
)
}
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
)
}
// This pattern is clean and reusable!Multiple Contexts
You can use multiple contexts in the same app:
// Create multiple contexts
const ThemeContext = React.createContext()
const UserContext = React.createContext()
const LanguageContext = React.createContext()
function App() {
const [theme, setTheme] = React.useState('light')
const [user, setUser] = React.useState(null)
const [language, setLanguage] = React.useState('en')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
<Dashboard />
</LanguageContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
)
}
// Consume multiple contexts
function Profile() {
const { theme } = React.useContext(ThemeContext)
const { user } = React.useContext(UserContext)
const { language } = React.useContext(LanguageContext)
return (
<div className={theme}>
<p>{language === 'en' ? 'Welcome' : 'Bienvenido'}, {user?.name}!</p>
</div>
)
}
// Better pattern: Combine related contexts
function AppProvider({ children }) {
return (
<ThemeProvider>
<UserProvider>
<LanguageProvider>
{children}
</LanguageProvider>
</UserProvider>
</ThemeProvider>
)
}
function App() {
return (
<AppProvider>
<Dashboard />
</AppProvider>
)
}Interactive Demo
š” Tip: Edit the code above and click "Run" to see your changes
Common Context Patterns
Pattern 1: Custom Hook for Context
// Create context and hook together
const AuthContext = React.createContext()
function AuthProvider({ children }) {
const [user, setUser] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const login = async (credentials) => {
setIsLoading(true)
const user = await api.login(credentials)
setUser(user)
setIsLoading(false)
}
const logout = () => {
setUser(null)
}
const value = { user, isLoading, login, logout }
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
// Custom hook with error checking
function useAuth() {
const context = React.useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Usage:
function LoginButton() {
const { user, login, logout } = useAuth()
if (user) {
return <button onClick={logout}>Logout</button>
}
return <button onClick={() => login(credentials)}>Login</button>
}
// Clean API!Pattern 2: Separate State and Actions
// Separate contexts for state and actions
const TodoStateContext = React.createContext()
const TodoActionsContext = React.createContext()
function TodoProvider({ children }) {
const [todos, setTodos] = React.useState([])
const actions = {
addTodo: (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }])
},
toggleTodo: (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
},
deleteTodo: (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
}
return (
<TodoStateContext.Provider value={todos}>
<TodoActionsContext.Provider value={actions}>
{children}
</TodoActionsContext.Provider>
</TodoStateContext.Provider>
)
}
// Components that only read don't re-render on actions change
function useTodos() {
return React.useContext(TodoStateContext)
}
function useTodoActions() {
return React.useContext(TodoActionsContext)
}
// Usage:
function TodoList() {
const todos = useTodos() // Only re-renders when todos change
return todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}
function AddTodoForm() {
const { addTodo } = useTodoActions() // Doesn't re-render when todos change
return <form onSubmit={e => { e.preventDefault(); addTodo(text) }}>...</form>
}Pattern 3: Context with Reducer
const CartContext = React.createContext()
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.item]
case 'REMOVE_ITEM':
return state.filter(item => item.id !== action.id)
case 'CLEAR':
return []
default:
return state
}
}
function CartProvider({ children }) {
const [cart, dispatch] = React.useReducer(cartReducer, [])
const value = {
cart,
addItem: (item) => dispatch({ type: 'ADD_ITEM', item }),
removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', id }),
clearCart: () => dispatch({ type: 'CLEAR' })
}
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
)
}
// useReducer + Context is powerful for complex state!Context Performance Considerations
Problem: All Consumers Re-render
const AppContext = React.createContext()
function AppProvider({ children }) {
const [user, setUser] = React.useState(null)
const [theme, setTheme] = React.useState('light')
const [language, setLanguage] = React.useState('en')
const value = { user, setUser, theme, setTheme, language, setLanguage }
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
)
}
function ThemeButton() {
const { theme, setTheme } = React.useContext(AppContext)
// Problem: Re-renders when user or language change!
return <button onClick={() => setTheme('dark')}>{theme}</button>
}
// When user changes, ThemeButton re-renders even though it doesn't use user!Solution 1: Split Contexts
// Create separate contexts
const UserContext = React.createContext()
const ThemeContext = React.createContext()
const LanguageContext = React.createContext()
function AppProvider({ children }) {
const [user, setUser] = React.useState(null)
const [theme, setTheme] = React.useState('light')
const [language, setLanguage] = React.useState('en')
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
)
}
function ThemeButton() {
const { theme, setTheme } = React.useContext(ThemeContext)
// Only re-renders when theme changes!
return <button onClick={() => setTheme('dark')}>{theme}</button>
}Solution 2: Memoize Context Value
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState('light')
// ā Creates new object every render
const value = { theme, setTheme }
// ā
Memoize to prevent unnecessary re-renders
const value = React.useMemo(() => ({
theme,
setTheme
}), [theme]) // Only recreate when theme changes
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}Context Performance Tips:
- Split contexts by concern (don't put everything in one)
- Memoize context values to prevent object recreation
- Use separate contexts for state and actions
- Don't use context for frequently changing values
- Consider state management libraries for complex apps
When to Use Context
ā Good Use Cases for Context:
- Theme/styling - Light/dark mode
- User authentication - Current user, login state
- Language/localization - i18n settings
- Global settings - App configuration
- Feature flags - A/B testing
ā Don't Use Context For:
- Frequently changing data - Use state management library
- Simple parent-child communication - Use props
- Performance-critical updates - All consumers re-render
- Data that only a few components need - Use props or lift state
š” Context Best Practices
⢠Use context for truly global data
⢠Create custom hooks for each context
⢠Provide good error messages
⢠Split contexts by domain
⢠Memoize context values
⢠Don't overuse - props are usually better
Key Takeaways
- Context solves prop drilling problem
- Three steps: Create, Provide, Consume
React.createContext()creates context<Context.Provider>provides value to treeuseContext(Context)consumes value- Create custom hooks for better API
- All consumers re-render when context value changes
- Split contexts to prevent unnecessary re-renders
- Memoize context values to prevent object recreation
- Best for global data like theme, auth, settings
- Don't use for frequently changing data
- Can use multiple contexts in same component
- Context is NOT a full state management solution
What's Next?
You now understand useContext and how to share state across your component tree! You know how to avoid prop drilling and when to use context effectively.
In the next lesson, you'll learn about useMemo Hook:
- What is memoization and why it matters
- Using useMemo to optimize expensive calculations
- Preventing unnecessary re-computations
- When to use useMemo vs when not to
- Common memoization patterns
Get ready to optimize your React applications with memoization! ā”