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:
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.