In this article, I will discuss how to implement React’s Context api and why you should use it.
What problem does the Context api solve?
When learning concepts in React or React Native, it’s vital you understand what problem a particular “tool” is solving. For instance, the React docs say the following about using custom hooks:
Custom Hooks let you share stateful logic…
What problem does this solve? The problem of repeating code! The example they give involves code a developer would typically implement when building a form.
return (
<>
<label>
First name:
<input value={firstName} onChange={handleFirstNameChange} />
</label>
<label>
Last name:
<input value={lastName} onChange={handleLastNameChange} />
</label>
<p><b>Good morning, {firstName} {lastName}.</b></p>
</>
);
Notice how they’re repeating the same “stateful” logic with value and onChange. To remedy this problem, the React docs suggest using a custom hook as seen below:
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}
function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// spread the objects received in each variable in your input elements
By creating one handler function and one state variable inside the useForm custom hook, a developer will avoid breaking the D.R.Y principle.What does all this have to do with the Context api? Everything! One of the things I learned from a Senior Developer was understanding the why behind a concept. When you understand the why behind a tool, you’re less likely to misuse said tool.
So why do we use the Context api? This question can be answered by asking “What problem or problems does it solve?” The answer, prop drilling!
Prop drilling
Passing props or “data” in React is a common task many React developers will engage in. But what happens when passing props becomes a “chore” because you have to pass data through many intermediate components like the following code below.
import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
export default function App() {
const [isLarge, setIsLarge] = useState(false);
const imageSize = isLarge ? 150 : 100;
return (
<>
<label>
<input
type="checkbox"
checked={isLarge}
onChange={e => {
setIsLarge(e.target.checked);
}}
/>
Use large images
</label>
<hr />
<List imageSize={imageSize} />
</>
)
}
function List({ imageSize }) {
const listItems = places.map(place =>
<li key={place.id}>
<Place
place={place}
imageSize={imageSize}
/>
</li>
);
return <ul>{listItems}</ul>;
}
function Place({ place, imageSize }) {
return (
<>
<PlaceImage
place={place}
imageSize={imageSize}
/>
<p>
<b>{place.name}</b>
{': ' + place.description}
</p>
</>
);
}
function PlaceImage({ place, imageSize }) {
return (
<img
src={getImageUrl(place)}
alt={place.name}
width={imageSize}
height={imageSize}
/>
);
}
Link to code here!
This is known as prop drilling. Notice how the “imageSize” prop gets passed through two intermediary components before it reaches its final destination in the PlaceImage component. Though this code might work, it could lead to bugs and unintentional mistakes down the road. To avoid these types of issues, React provides us with the Context “mechanism”.
How to use Context
When implementing the Context api in a React project, the React docs explain its implementation like this:
Create a context…
Use that context…
Provide that context…
Here’s an example of all three steps. The implementation below is something you may see in production code or in the “wild”.
import { createContext, useContext } from "react";
export const ImageSizeContext = createContext();
export const ImageSizeContextProvider = ({ children, value }) => {
return (
<ImageSizeContext.Provider value={value}>
{children}
</ImageSizeContext.Provider>
);
};
export function useImageSizeContext() {
const value = useContext(ImageSizeContext);
return value;
}
Let’s walk through this code.
ImageSizeContext - This variable holds an object being returned by the React.createContext() function. When you call React.createContext(), pretend as though you’re creating a “store” without putting any products in it (unless you give it a default value). We will place “products” or data in our store with the object being returned to ImageSizeContext.
ImageSizeContextProvider - This function returns a component from our ImageSizeContext variable. This is where we take our empty store and put “items” in it to be shared with the public or “children” in our case. And where do these items comes from? The value prop.
useImageSizeContext- This custom hook utilizes the React.useContext function. The useContext hook is “how” our components (or customers) will receive data given to the ImageSizeContextProvider . The reason we put the useContext hook in a function is so we don’t repeat ourselves every time we need to use the useContext hook. If we didn’t create a function, we would have to import ImageSizeContext and call the useContext hook in every component, breaking the D.R.Y rule.
Let’s see what our code looks like from our previous example, concerning the “imageSize” prop, after using the Context api.
import { useState, useContext } from "react";
import { places } from "./data.js";
import { getImageUrl } from "./utils.js";
import { ImageSizeContextProvider, useImageSizeContext } from "./Context.js";
export default function App() {
const [isLarge, setIsLarge] = useState(false);
const imageSize = isLarge ? 150 : 100;
return (
<ImageSizeContextProvider value={imageSize}>
<label>
<input
type="checkbox"
checked={isLarge}
onChange={(e) => {
setIsLarge(e.target.checked);
}}
/>
Use large images
</label>
<hr />
<List />
</ImageSizeContextProvider>
);
}
function List() {
const listItems = places.map((place) => (
<li key={place.id}>
<Place place={place} />
</li>
));
return <ul>{listItems}</ul>;
}
function Place({ place }) {
return (
<>
<PlaceImage place={place} />
<p>
<b>{place.name}</b>
{": " + place.description}
</p>
</>
);
}
function PlaceImage({ place }) {
const imageSize = useImageSizeContext();
return (
<img
src={getImageUrl(place)}
alt={place.name}
width={imageSize}
height={imageSize}
/>
);
}
Link to original challenge here
Now the PlaceImage component doesn’t have to rely on other components to receive its imagesSize value and we save our future selves the headache of dealing with complicated bugs.
Recap:
Remember to understand the why behind a tool. And one way to do this is to ask:
”What problem does this tool solve?”
The Context api solves the issue of prop drilling. It can be used in other scenarios as well. Click here to learn more.
To implement the Context api, you need to remember these steps:
Create a context - build the store building. This store will hold the data our components need
Use that context - allows the “customers” or children to buy items
Provide that context - Put items in the store. This is the data our components will have access to once the Provider component wraps around a parent component.