Okay, React Context API is among us for a while now, since version 16.3 of React. But should we use it? And, most importantly, how can we use React Context API properly and what kind of problem we can solve with it?
In this discussion I am assuming the reader has a simple knowledge about React and React hooks API.
The problem
In order to show the usual problem to be solved with React Context API, I created a very simple React App. I just used the boilerplate of create react app
npx create-react-app react-context
and, once the application was ready I created the folder components inside src and inside of it I added the folders App, ProductsList and ProductItem as shown in the following image.
Here is the code of each file.
App/index.jsx
import React, { useState } from 'react';
import ProductsList from '../ProductsList';
const App = () => {
const initialProducts = [
{ id: 1, name: 'Apple', price: 0.45 },
{ id: 2, name: 'Onion', price: 0.54 },
{ id: 3, name: 'Meat', price: 3.55 },
{ id: 4, name: 'Milk', price: 0.86 },
{ id: 5, name: 'Bread', price: 1.18 },
];
const [products, setProducts] = useState(initialProducts);
const handleDeleteProduct = (product) => {
const productElements = products.filter(prod => prod.id !== product.id);
setProducts(productElements);
};
return (
<ProductsList products={products} deleteProduct={handleDeleteProduct} />
);
}
export default App;
ProductsList/index.jsx
import React from 'react';
import ProductItem from '../ProductItem';
const ProductsList = ({ products, deleteProduct }) => {
return (
<div style={{ marginLeft: '10px' }}>
<h3>Products List</h3>
<div>
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
deleteProduct={deleteProduct}
/>
))}
</div>
</div>
);
};
export default ProductsList;
ProductItem/index.jsx
import React from 'react';
const ProductItem = ({ product, deleteProduct }) => {
return (
<div style={{
backgroundColor: '#646FD4',
borderRadius: '5px',
width: '120px',
marginBottom: '10px'
}}>
<div style={{
textAlign: 'center',
fontSize: '18px',
fontWeight: 'bold',
color: '#F5F5F5'
}}>
{product.name}
</div>
<div style={{
textAlign: 'center',
fontSize: '14px',
fontWeight: 'bold',
color: '#F5F5F5'
}}>
{product.price.toLocaleString("en-IE", {style:"currency", currency:"EUR"})}
</div>
<div style={{
marginTop: '5px',
textAlign: 'center',
}}>
<button
style={{
backgroundColor: '#F32424',
border: '1px solid #F32424',
borderRadius: '5px',
padding: '6px 8px',
color: '#FFFFFF',
fontWeight: 'bold',
marginBottom: '5px',
}}
onClick={() => deleteProduct(product)}
>
Apagar
</button>
</div>
</div>
);
};
export default ProductItem;
Important thing here: I do not use css styles inline as shown in the code above. I'm just passing it inline because I wanted a very simple style instead of show you all the raw html. So, sorry for that! I hope you understand me.
Well, basically, the App component has an array of products as its state and a function that delete from that array some specific product. It only renders the ProductList component that receives both the array of products and the delete function as props.
The ProductsList component, as mentioned above receives the array of products and uses the map method to render to each product of the array one instance of the ProductItem component.
The ProductItem component just receives the product and the deleteProduct function as props and renders a simple div, showing the name and price of product and a button that, once clicked, deletes that product.
After all that effort, let's see what we have just created
Okay! Pretty good! Now, let's understand the hierarchy tree of our application so far.
It's just a simple hierarchy tree but it's enough to understand what is happening: despite the function handleDeleteProduct is created in the App component, since it need the use the products array to filter the element to be deleted, it will only be used in the ProductItem component and, even though, the ProductsList component needs to receive it by the App component to pass it, again as props to its son, the ProductItem component. In this situation, the ProductsList component is just a bridge where the prop needs to pass so it can be delivered into the component that will really use it.
This is called prop drilling, which as the name suggests, is basically a situation when the same data is being sent at almost every level due to requirements in the final level. Every React developer will face this situation eventually. You could tell me, come on, this is not a problem at all to pass it through the ProductList component but believe me, in a real world application, with a huge hierarchy tree, you can face the problem of one parent component passing props to a child component after several nodes in different directions. And yes, this can become a hell. More than that, in a real world application, we usually deal with other libraries in our projects and we need to use every single prop in those. For example, generally I use the prop-types library. Using this library, we need to set the type of each prop of the component and yes, it is very annoying to pass the same data to several components just because of some leaf component needs to use one element from the root or from some node above.
Okay, after this, kind of big explanation, how can we avoid the prop drilling? Well, let's think about functions in programming. You can have some variables defined locally or globally. Every single global variable can be used in any function as a parameter but none local variables can be used globally. So, by that logic, we should think about those data globally, right?
Right. Actually, roughly speaking, from the React point of view, our components play the same role as functions (remember the name function components?) in procedural programming and props are, exactly what you are thinking, local variables. And yes, React Context API will basically allow us to use some data globally in our components. That way, every single state in the context can be passed directly only to those components that really need to use it.
The solution
Well, basically, the solution requires 3 steps:
- Create the context;
- Provide the context;
- Consume the context;
Let's start with creating our context. In order to do that, I usually create a new folder inside src, called context, according to the following image.
Inside the context folder, we create a new file named ProductsContext.jsx with the following code
ProductsContext.jsx
import { createContext } from 'react';
const ProductsContext = createContext({});
export const ProductsProvider = ProductsContext.Provider;
export default ProductsContext;
We use the createContext function to (guess what?) create a the context and we store it in the variable ProductsContext and then, we use the ProductsContext.Provider to generate a wrapper from which we will deliver values to all components inside this wrapper. This provider is stored in the variable ProductsProvider.
Once this is done, we go back to the App component and import the provider.
import { ProductsProvider } from '../../context/ProductsContext';
After that, we wrap up the everything in the return of the App component and we pass the handleDeleteProduct function as a method of the value object of the provider. Roughly speaking, the handleDeleteProduct is kind of becoming one global variable (in this case a function) that can be used to any component child of the App component. We also remove the props deleteProduct that was initially being passed to the ProductsList component.
App/index.jsx
import React, { useState } from 'react';
import { ProductsProvider } from '../../context/ProductsContext';
import ProductsList from '../ProductsList';
const App = () => {
const initialProducts = [
{ id: 1, name: 'Apple', price: 0.45 },
{ id: 2, name: 'Onion', price: 0.54 },
{ id: 3, name: 'Meat', price: 3.55 },
{ id: 4, name: 'Milk', price: 0.86 },
{ id: 5, name: 'Bread', price: 1.18 },
];
const [products, setProducts] = useState(initialProducts);
const handleDeleteProduct = (product) => {
const productElements = products.filter(prod => prod.id !== product.id);
setProducts(productElements);
};
return (
<ProductsProvider value={{ deleteProduct: handleDeleteProduct }}>
<ProductsList products={products} />
</ProductsProvider>
);
}
export default App;
Since we don't have to use the handleDeleteProduct inside the ProductsList component, we remove the deleteProduct as a prop of it and, of course, we do not have to pass it as props into the ProductItem
ProductsList/index.jsx
import React from 'react';
import ProductItem from '../ProductItem';
const ProductsList = ({ products }) => {
return (
<div style={{ marginLeft: '10px' }}>
<h3>Products List</h3>
<div>
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
/>
))}
</div>
</div>
);
};
export default ProductsList;
Now, in the ProductItem component, we need to call the context in order to use the deleteProduct method to delete the product item. To do that, we make use of the useContext hook. So, we import the useContext from the React library and the ProductContext and just create a variable value to store the (again!) value of the context by making use of the useContext hook. To finish, we remove the deleteProduct as a received prop of ProductItem component and call the value.deleteProduct in the onClick of the delete button.
ProductItem/index.jsx
import React, { useContext } from 'react';
import ProductsContext from '../../context/ProductsContext';
const ProductItem = ({ product }) => {
const value = useContext(ProductsContext);
return (
<div style={{
backgroundColor: '#646FD4',
borderRadius: '5px',
width: '120px',
marginBottom: '10px'
}}>
<div style={{
textAlign: 'center',
fontSize: '18px',
fontWeight: 'bold',
color: '#F5F5F5'
}}>
{product.name}
</div>
<div style={{
textAlign: 'center',
fontSize: '14px',
fontWeight: 'bold',
color: '#F5F5F5'
}}>
{product.price.toLocaleString("en-IE", {style:"currency", currency:"EUR"})}
</div>
<div style={{
marginTop: '5px',
textAlign: 'center',
}}>
<button
style={{
backgroundColor: '#F32424',
border: '1px solid #F32424',
borderRadius: '5px',
padding: '6px 8px',
color: '#FFFFFF',
fontWeight: 'bold',
marginBottom: '5px',
}}
onClick={() => value.deleteProduct(product)}
>
Delete
</button>
</div>
</div>
);
};
export default ProductItem;
Well, that's it! We finish the task and we can use the handleDeleteProduct function defined in the App component in a non-direct of its child components. Again, this was just a simple application specifically created to show the prop drilling problem and how to solve it using the React context API. It can be tempting to say that we increase the difficulty of the example (which I definitely agree, considering only these components), but in a real world application, the React context API really increase your productivity.
An important observation to make here is that this context was very simple. But we can create contexts that passes through more than just a simple javascript function, but also entirely other components. For example, I use context to pass through alerts and modals components. This increases the productivity a lot, since every time I need to show an alert or a modal in any page, I just need to call the respective context as a function. I can also notice that many people is using contexts today as an alternative to Redux.
Top comments (0)