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.
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.
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:
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
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 the
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 a
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 a
Consumer 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!
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.