Introduction
React is a component-based library that allows developers to build dynamic user interfaces by managing state and passing data between components. While props are the primary way to pass data from parent to child components, managing state across deeply nested components can become cumbersome. This problem, known as prop drilling, occurs when data must be passed through intermediate components that do not need it.
The React Context API solves this problem by providing a way to share data globally across the component tree without explicitly passing props at every level. The useContext hook makes it easy for functional components to access context values.
In this post, we will explore:
- Creating context
- Providing context values
- Consuming context in child components
- When to use context versus prop drilling
By the end of this post, you will have a comprehensive understanding of how to use useContext for global state management in React.
What is Context in React?
Context provides a way to share values like state, functions, or theme data across multiple components without passing props manually through every level. It is particularly useful for global data that many components need, such as:
- User authentication state
- Theme settings (dark/light mode)
- Language preferences
- Application settings
1. Creating Context
The first step is to create a context object using React.createContext. This object will hold the global data and provide methods to access it.
Example: Creating a Theme Context
import React from "react";
// Create a context with a default value
const ThemeContext = React.createContext("light");
export default ThemeContext;
Explanation:
ThemeContextis now a context object."light"is the default value, used when a component consuming the context does not have a matching provider above it in the tree.
2. Providing Context Values
To share data, you need a Context Provider. The provider wraps components and supplies context values.
Example: Providing Theme Value
import React, { useState } from "react";
import ThemeContext from "./ThemeContext";
import Toolbar from "./Toolbar";
function App() {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div>
<Toolbar />
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
</ThemeContext.Provider>
);
}
export default App;
Explanation:
ThemeContext.Providerwraps the components that need access to the context.- The
valueprop contains the data we want to share. Here, it is an object containingthemeandtoggleTheme. - Components inside the provider can access this data without receiving props from the parent.
3. Consuming Context in Child Components
The useContext hook allows functional components to consume context values easily.
Example: Using Theme Context in a Child Component
import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";
function Toolbar() {
const { theme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === "light" ? "#fff" : "#333", color: theme === "light" ? "#000" : "#fff" }}>
<p>Current theme: {theme}</p>
</div>
);
}
export default Toolbar;
Explanation:
useContext(ThemeContext)returns the current context value ({ theme, toggleTheme }).- No props need to be passed from
ApptoToolbar. - Changes in context automatically re-render all consuming components.
Example: Consuming Multiple Values
If you have multiple context values, you can consume them in a single component:
import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";
import UserContext from "./UserContext";
function Dashboard() {
const { theme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
return (
<div style={{ backgroundColor: theme === "light" ? "#eee" : "#222", color: theme === "light" ? "#000" : "#fff" }}>
<h1>Welcome, {user.name}</h1>
<p>Theme: {theme}</p>
</div>
);
}
- Multiple
useContexthooks can be used in the same component.
4. When to Use Context vs Prop Drilling
Prop Drilling
- Definition: Passing data through multiple intermediate components that don’t need it.
- Example: Passing
themefromApptoToolbarthrough several nested components. - Problem: Makes the code verbose and harder to maintain.
function App() {
const theme = "dark";
return <Parent theme={theme} />;
}
function Parent({ theme }) {
return <Child theme={theme} />;
}
function Child({ theme }) {
return <Toolbar theme={theme} />;
}
- Each intermediate component must receive and pass the prop.
Context
- Definition: Provides a way to share data directly with the components that need it.
- Benefits:
- Reduces unnecessary props.
- Simplifies component trees.
- Makes components more reusable.
- Example: Using
useContexteliminates intermediate props:
function Toolbar() {
const { theme } = useContext(ThemeContext);
return <div>{theme}</div>;
}
When to Use Context
- Global state or data needed by multiple components.
- Themes or UI settings.
- User authentication and roles.
- Language or localization data.
- Avoid using context for frequently changing high-volume data like input fields to prevent unnecessary re-renders.
Advanced Example: User Authentication
Creating User Context
import React from "react";
const UserContext = React.createContext(null);
export default UserContext;
Providing Context
import React, { useState } from "react";
import UserContext from "./UserContext";
import Dashboard from "./Dashboard";
function App() {
const [user, setUser] = useState({ name: "Alice", role: "admin" });
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
export default App;
Consuming Context in Dashboard
import React, { useContext } from "react";
import UserContext from "./UserContext";
function Dashboard() {
const { user } = useContext(UserContext);
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Role: {user.role}</p>
</div>
);
}
export default Dashboard;
Combining Multiple Contexts
When an application requires multiple contexts (theme, user, settings), you can nest providers or create a single provider combining multiple contexts.
function App() {
return (
<ThemeContext.Provider value={{ theme: "light" }}>
<UserContext.Provider value={{ user: { name: "Alice" } }}>
<Dashboard />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
- Components can consume any context they need using
useContext.
Context with Dynamic Updates
Context values can be updated dynamically, and consuming components automatically re-render.
function ToggleThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return <button onClick={toggleTheme}>Switch to {theme === "light" ? "Dark" : "Light"} Mode</button>;
}
- The
toggleThemefunction updates the context state in the provider. - All components consuming
themereflect the change immediately.
Advantages of useContext
- Eliminates prop drilling.
- Makes components cleaner and easier to read.
- Centralizes shared state.
- Works well with functional components.
- Simple API with
createContextanduseContext.
Limitations of useContext
- Not a replacement for state management libraries: For complex apps, Redux, Zustand, or Recoil may be better.
- Frequent updates can re-render all consumers: Avoid putting rapidly changing data in context.
- Nested providers can become verbose: Consider combining providers.
Best Practices
- Only store global data in context.
- Keep context values stable using
useMemooruseReducerif needed. - Avoid storing transient UI state like input values in context.
- Name context clearly, e.g.,
ThemeContext,UserContext. - Provide default values to avoid undefined errors.
Leave a Reply