React Context in the World of Component Composition

If you ever tried React’s Context API before it officially became a stable feature, you probably remember all the warnings and red lights in the documentations that discouraged us from using it. Since React 16.3, the Context API is no longer experimental — it is an advanced feature that was designed to help developers avoid the “Prop Drilling” problem.

Essentially, Context enables developers to write React components that live deep down the component tree and consume data from higher up, without the need of passing that data through intermediate components via props manually:

“Context is designed to share data that can be considered “global” for a tree of React components” (React Docs)

The documentation, as well as other examples that show the use of Context, describe cases where common data such as locale or theme is stored on a higher level of the tree and is consumed, using the API, by components further down.

In this post, I will try to describe a different approach to using Context, and the benefits of it when thinking of component composition.

The Menu

Let’s assume (you can close your eyes and imagine) we have a beautiful application that has grown slightly more complex than we first imagined. We would like to make it easier for our users to navigate around it so we decide to add a navigation menu. Some of the pages in our application have subpages, and in turn these subpages can have additional subpages of their own. The product requirement is that when a user is viewing a certain page in our app, the corresponding item in the menu is highlighted. If a highlighted page is a subpage, meaning it has a parent page, the parent page is also highlighted.

In our app, Item 2.2.1 is the currently selected subpage. It’s ancestors in the menu are also highlighted.

As React developers, we like to think about reusable components. If you are familiar with Atomic Design, you are perhaps already thinking of our menu as an interface made of reusable menu items. Soon enough, if you were to implement this menu, you would probably get something like this:

This is pretty straightforward — we have our menu, which consists of reusable MenuItems. The 2nd MenuItem uses the children prop to pass MenuItems that will be rendered in the menu as subpages. Each item has 3 props: label, isSelected and onClick. Whenever a menu item is clicked, we will simply check which other menu items should be highlighted and update the selected items in the component’s state.

But something about this code should bother us.

It doesn’t feel like “Prop Drilling” — our menu is a “flat” composition of components and we are not passing data deep down the component tree. Instead, we are repeating ourselves. We are repeating the way we are passing props to the MenuItems. What if we changed the way we think of MenuItemas a component on its own?

Conceptually, most reusable UI components we write can be used anywhere: you would place the component somewhere in your code, pass some props and watch it render. A simple button, for instance, can serve different purposes throughout an app and exist as its own independent unit. If we apply the same idea to something like a menu item we notice something interesting: a menu item, as a component, cannot exist independently. Sure, you could place it anywhere and reuse it as much as you want — but does it really make sense without a Menu?

Giving The Menu Items Some Context

Let’s take another look at our example above. We are going to leverage the power of React’s Context to literally put MenuItem in context:

First, we use React.createContext to create a Provider and a Consumer. Just in case, a default value is passed as an argument of createContext which will be used if a consumer is rendered without a matching provider. You can read more about it here.

Next, we create a MenuContextProvider that will wrap our MenuItems and take care of the logic necessary to mark items as selected. In our example, it is the itemKeys in the selectedItems array. The render function of the context provider simply passes the array of selected item keys and a click handler down using Provider. This data will be consumed by the MenuItems.

Finally, we change MenuItem a little bit. Every provider needs a consumer, so take a look at the render function: It uses the Consumer component to subscribe to context changes. It also uses a function as a child to receive the context value, which is then passed to renderMenuItem to actually render it, similar to our original example.

Also, notice that a menu item is selected simply by checking if the itemKey exists in the array of selected menu items. Deciding whether or not the key is in the array depends on logic managed by the menu itself.

What have we gained here?

By defining the role of menu items within a menu, we created a relationship that allows menu item components to remain reusable within a boundary — that is the menu. Because the menu holds the logic of which item should be selected, as well as notifying of any item clicks (and potentially additional events), we were able to remove both theisSelected and onClick props from MenuItem. This makes the menu extendable in the future and keeps the code clean, readable and explicit while maintaining any calculations under the hood.

Another important thing worth mentioning is that context consumers read the context from the closest matching provider above them in the component tree. This means that we can create rather complex menus with nested menu items and still keep our code consistent and well structured.

Thinking of Other Use Cases

Have you ever used a radio button? If we consider the radio button as a standalone component, it is easy to see that it only serves it’s purpose as part of a group. Let’s take a quick look at a short example where RadioButton can be composed as part of aRadioGroup :

We achieve this kind of structure by using Context in a similar way we did with the menu example above. The RadioGroup component wraps a React Context Provider and the RadioButtons consume the data (in this case, the selected value) with the help of aConsumer component. A RadioButton is rendered in it’s selected state if the selected value matches the value that was passed to it using the value prop. Radio button clicks are handled by calling a function passed by the context. This allows us to encapsulate the logic needed to decide which radio button is selected as part of the RadioGroup component. Simple!

Conclusion

React makes it easy for us to write reusable code. If we are talking about UI, writing reusable components is a breeze and it makes our lives as developers much easier when it comes to using them throughout our application. However sometimes, reusable UI components are meaningless on their own. Instead, we can think of reusable relationships. For example, the relationship between menu items and the menu itself helps define a group of coupled components, making the group itself reusable.

You can apply this idea next time you are repeating yourself when making a complex component composition. This can be select options within a select dropdown, different types of form fields within a form, or even line graphs on a chart. While there are various ways to solve each problem, you can consider thinking of React’s Context API as more than just a way to avoid passing props further down the tree. Instead, use its power to give context to the components that need it.