Using React Context to avoid prop drilling on a Next.js generated page
Many developers use React Context for state management in React and Next.js application. Whilst there is nothing wrong with this, the React Context API is far more powerful and has a lot more to offer than just state management.
The React Context API can make Next.js code easier to read and maintain by evading the need to prop drill on large and complex pages.
In this post, I will discuss prop drilling in Next.js applications and then demonstrate how it can be improved using React Context. I hope that this post will show you how React Context can make complex pages in Next.js easier to build and give you an introduction to using Context to access Next.js Page props.
What is prop drilling?
Prop drilling is best described by highlighting a scenario with several layers of nested components. If you have experience building complex React applications where multiple components need access to the same data it is likely you have experienced prop drilling. It's usually at this point you'll begin to explore state management options.
Say you have ComponentA
which takes a prop called name
. You would use ComponentA
like this:
<ComponentA name="technouz" />
But what if ComponentA
doesn't actually need to render anything based on the name
property? What if ComponentA
only takes the name
property to pass it to it's child: ComponentB
?
And then, what if ComponentB
, ComponentC
and ComponentD
don't need to use the value for name
… but only pass it down to be finally used in ComponentE
?
Your component tree will look something like this:
// note: this is not an entirely accurate representation of a React component tree
<ComponentA name="technouz">
<ComponentB name={name}>
<ComponentC name={name}>
<ComponentD name={name}>
<ComponentE name={name}>
Hi! My name is {name}!
</ComponentE>
<ComponentD>
<ComponentC>
<ComponentB>
<ComponentA>
Do you notice how messy it is to keep passing down the name
prop from ComponentA
to ComponentE
? This is particularly difficult to maintain if none of the components between ComponentA
and ComponentE
actually need to use the name
prop.
Passing down the prop like this is called prop drilling. As a developer, you are having to drill the name
prop from ComponentA
to ComponentE
and that's where the practise gets its name from - Prop Drilling.
Is prop drilling always bad?
Not necessarily. In one-off cases, I think prop drilling is fine. By design, React allows for prop drilling and it's a very efficient way to get a value from a parent component to a child component.
However, when you're building larger applications you don't want to prop drilling multiple props across multiple components over multiple pages. It gets very, very messy!
Thankfully, React Context can help.
Introducing React Context to a Next.js application
Adding React Context to a Next.js application is no different to a React application. The Next.js documentation does a great job of explaining how to do this, and I've adapted the docs to create a short guide here.
If you follow the guide linked above you'll end up with a Context file similar to the one shown below.
For this post, I'll be using a page which shows details about a restaurant as an example. So, we'll call the context RestaurantContext
.
// context/restaurant-context.js
import { createContext, useContext } from 'react';
const RestaurantContext = createContext();
export function RestaurantProvider({ children }) {
const [restaurant, setRestaurant] = useState(null);
const value = { restaurant, setRestaurant };
return (
<RestaurantContext.Provider value={value}>
{children}
</RestaurantContext.Provider>
);
}
export function useRestaurant() {
return useContext(RestaurantContext);
}
The above Context initialises the value to null
and we expose a setRestaurant()
method that can be used to set the value for restaurant
. All of this is persisted to memory using the useState()
hook within the scope of the RestaurantContext
.
To use RestaurantContext
in a Next.js page, you'll need to change the structure a little.
The RestaurantContext
can only be accessed by components wrapped under the <RestaurantProvider />
. This is standard React Context API practise. So, in the Next.js page we export a wrapper which wraps a <Screen />
component with <RestaurantProvider />
and this will give us the access we need to the RestaurantContext
.
export default function RestaurantPage(props) {
return (
<RestaurantProvider>
<Screen {...props} />
</RestaurantProvider>
);
}
function Screen({ restaurantFromServer }) {
const { restaurant, setRestaurant } = useRestaurant();
useEffect(() => {
setRestaurant(restaurantFromServer);
}, [restaurantFromServer]);
if (!restaurant) {
return (<div>Loading...</div)
}
...
// render the restaurant page if restaurant isn't null
}
Finally, the Next.js page above uses getStaticProps()
to load the restaurant data on the server-side. Therefore, we can use the setRestaurant()
method exposed through our custom useRestaurant()
hook to set the RestaurantContext
state to the restaurant object.
Accessing Context data in child components
Let's say our Restaurant page has the following component tree before introducing React Context:
<RestaurantPage>
<Screen>
<MainSection>
<ReviewSection>
<ListOfReviews>
</ListOfReviews>
</ReviewSection>
</MainSection>
</Screen>
</RestaurantPage>
In order to render the list of reviews we need access to the property restaurant.reviews
. Before using React Context, we would have to pass restaurant.reviews
as a prop from <Screen />
to <MainSection />
to <ReviewSection />
and finally to <ListOfReviews />
where we can render the data.
After introducing the RestaurantContext
, the Screen
component and all components under it are wrapped under the RestaurantProvider
. So the component tree actually looks like this:
<RestaurantPage>
<RestaurantProvider>
<Screen>
<MainSection>
<ReviewSection>
<ListOfReviews>
</ListOfReviews>
</ReviewSection>
</MainSection>
</Screen>
</RestaurantProvider>
</RestaurantPage>
This means we can remove all props that pass the value restaurant.reviews
from <Screen />
to <ListOfReviews />
and instead use the useRestaurant()
hook within the <ListOfReviews />
component like this:
export default function ListOfReviews() {
const { restaurant } = useRestaurant();
return(
<div class="container">
<h3>List of reviews</h3>
{restaurant.reviews.map(review => (
<div key={review.id}>
{review.name} ({review.rating})
</div>
))}
</div>
);
}
So instead of passing the value for reviews down the component tree as a prop:
export default function ListOfReviews({ reviews }) {
...
We can access it through the useRestaurant()
hook:
export default function ListOfReviews() {
const { restaurant } = useRestaurant();
const { reviews } = restaurant;
...