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