React’s useContext hook is a powerful tool that allows you to share state and logic across multiple components in your application without having to pass props down through multiple levels of components. It allows you to access a context object (created by a Context.Provider) from a component’s tree without having to manually pass the context down through every level of the tree.
To use the useContext hook, you first need to create a context using the React.createContext method. This method takes an initial value as an argument, which is the default value for the context when it is accessed for the first time.
Here’s an example of creating a context for a theme:
import { createContext } from 'react';
const ThemeContext = createContext('light');
To make the context available to your components, you need to wrap them in a Context.Provider component. The provider takes a value prop, which is the current value of the context. Any component that is a descendant of the provider can access the context value using the useContext hook.
Here’s an example of using the theme context in a component:
import { useContext } from 'react';
import { ThemeContext } from './theme-context';
function MyComponent() {
const theme = useContext(ThemeContext);
return (
<div className={`theme-${theme}`}>
{/* component content */}
</div>
);
}
In this example, the MyComponent component uses the useContext hook to access the theme context. The theme context is set by a parent component that wraps the MyComponent component in a `ThemeContext.Provider
here are a few more examples of how you might use the useContext hook in different scenarios:
Table of Contents
1. Sharing state across multiple components
import { createContext, useState } from 'react';
const UserContext = createContext();
function UserProvider({children}) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
function Header() {
const { user } = useContext(UserContext);
return (
<header>
<nav>
<span>Welcome, {user.name}</span>
</nav>
</header>
);
}
function LoginForm() {
const { setUser } = useContext(UserContext);
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const newUser = {
name: formData.get('name'),
email: formData.get('email')
};
setUser(newUser);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" name="name" />
</label>
<label>
Email:
<input type="email" name="email" />
</label>
<button type="submit">Log in</button>
</form>
);
}
function App() {
return (
<UserProvider>
<Header />
<LoginForm />
</UserProvider>
);
}
In this example, the UserProvider component uses the useState hook to manage the state of the current user. It wraps its children in a UserContext.Provider component, which makes the user state and setUser function available to any component that is a descendant of the provider. The Header component uses the useContext hook to access the user state and display a welcome message, and the LoginForm component uses the useContext hook to access the setUser function and update the user state when the form is submitted.
2. Sharing logic across multiple components
import { createContext, useState } from 'react';
const CartContext = createContext();
function CartProvider({children}) {
const [cart, setCart] = useState([]);
const addItem = (item) => {
setCart([...cart, item]);
};
const removeItem = (itemId) => {
setCart(cart.filter(item => item.id !== itemId));
};
return (
<CartContext.Provider value={{cart, addItem, removeItem}}>
{children}
</CartContext.Provider>
);
}
function ProductList() {
const { cart, addItem } = useContext(CartContext);
const handleAddToCart = (item) => {
addItem(item);
};
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
<button onClick={() => handleAddToCart(product)}>Add to cart</button>
</li>
))}
</ul>
);
}
function Cart() {
const { cart, removeItem } = useContext(CartContext);
const handleRemoveFromCart = (itemId) => {
removeItem(itemId);
};
return (
<ul>
{cart.map((item) => (
<li key={item.id}>
{item.name} - ${item.price}
<button onClick={() => handleRemoveFromCart(item.id)}>Remove</button>
</li>
))}
</ul>
);
}
function App() {
return (
<CartProvider>
<ProductList />
<Cart />
</CartProvider>
);
}
In this example, we have an e-commerce application that has two main components ProductList and Cart, that both need to access the cart state and logic, but they are not directly related, so we can’t pass the props down through the components.
To share the cart state and logic, we create a CartContext using the createContext method, providing an initial value, but in this case, we don’t need any. Then, we use the CartProvider component that wraps all the components that need to access the cart state and logic.
CartProvider component uses the useState hook to manage the state of the cart, it defines the addItem and removeItem functions, and then it wraps its children with a CartContext.Provider component that makes the cart state and these functions available to any component that is a descendant of the provider.
ProductList component uses the useContext hook to access the cart state and the addItem function, it maps over the products, and renders a button that when clicked, it calls the handleAddToCart function passing the product, this function then calls the addItem function that adds the product to the cart.
Cart component uses the useContext hook to access the cart state and the removeItem function, it maps over the items in the cart, and renders a button that when clicked, it calls the handleRemoveFromCart function passing the product id, this function then calls the removeItem function that removes the product from the cart.
Finally, the App component renders the ProductList and Cart component wrapped with the CartProvider component. This way, both ProductList and Cart components have access to the cart state and logic, and can update and retrieve the state as needed. This allows for a centralized way of managing state and logic, making it easier to maintain and debug the application. Additionally, it also improves performance by avoiding unnecessary re-renders of components that are triggered by changes in the cart state.
Also, it’s important to note that, useContext hook is a way to access the data that is provided by a Context.Provider component, but it doesn’t provide a way to update the data, that’s why we need to use state hooks like useState to manage the data and then pass the state and the logic to the context object.
Summary:
The best example of using the useContext hook would depend on the specific use case and requirements of your application. But in general, a good example of using the useContext hook would be when you have a piece of state or logic that is used by multiple components in your application, and passing it down through props would create a deep and complex component tree.
For example, imagine you have an application that has a theme that should be applied to all components, so you have a ThemeContext that holds the current theme and a ThemeProvider component that wraps your app components and provides the theme value.
In this case, any component that needs to know the current theme can use the useContext hook to access it, without the need of passing it down as a prop through multiple levels of components. This makes it easy to maintain, and change the theme without affecting the other components.
Another example would be an e-commerce application that needs to share the cart state and logic across multiple components, in this case, you can create a CartContext that holds the current cart state and the functions to add or remove items from the cart and use CartProvider component to provide the context to the components that need it.
These are just examples of how you might use the useContext hook in different scenarios, but it’s important to consider the specific needs of your application and use the hook in a way that makes the most sense for your use case.
