TABLE OF CONTENTS
This post explores the new react context API. We will explore why we need react context, how it works high-level with example use cases and at what point should React developers reach out to context API to solve their problems. Our sample use case will then show you how to use React context with some examples and comparing React context with Redux, an external package solution used before the React context API was introduced into the library core.
React Context API: What it is and How Does it Work?
When the React library was introduced into the community some years back, it was accepted and soon gained lots of popularity as the choice for building out user interfaces in a composable way. The major idea was that each UI interface can be split into multiple different small components and at the end of the day, these components can be combined or composed to form the whole larger UI as intended.
While this seemed like a clever idea and intuitive for small to medium scale applications, developers had a pertinent problem building out complex architectures that would span hundreds of different composable interfaces. This problem was, passing down props or sharing data known as state, across these different components became tedious as the scope of the problem grew.
As a background to what we are trying to present, if we were building multiple UI components for example, we indeed have a component tree which includes the parent component, which becomes the source of truth for our data, and due to the interrelation dependence on the parent components and those underneath it known as the children or descendant component to share data, it comes a point where this becomes an issue.
As we stated earlier, for small to medium apps, sharing data across many different components could be easy, since all we need to do is pass this data or props across or down from the parent to every child that needs it. Now this is fine. What if we have a hugely nested or a large component tree and we intend to pass the data or prop down this tree?
Do we still take the route of passing data down to every level until we get to that hundredth component that needs this data? Well, then, it becomes a bummer. This is what React context tries to solve. Before we begin to explore this API, let us be aware that before it was introduced, we had to rely on an external library or package known as Redux to deal with issues like this.
We usually reach out for Redux when we discover that the component tree is becoming too complex and sharing data across our component is becoming a tedious process. While this worked for most of us, Redux came with its own challenges.
Firstly, Redux came with a lot of boilerplate code we need to come to terms with. It also introduced a learning curve for new developers coming to our code base. Lastly, it adds another layer of external dependencies we needed to keep track of. Redux is not the purpose of our post, so we will not be dwelling much on it until we compare context and redux.
What is React Context?
React context, a core React API provides an easier interface for developers to share data or pass props down multiple levels deep in our React applications. From the React docs, with react context, we can easily pass data down to the very component that needs it at any level in the component tree, without having to explicitly pass this data down each component level in the tree. This is all there is to React context.
To drive this definition further for more clarity, say you have a particular theme color set at the parent component level of the app, and you only intend to pass it down to the thirtieth component down the tree, without having to pass it down every level until you get to that thirtieth level, but just pass it down to only the thirtieth component, is what react context provides for us out of the box. Let us see an example of this with a simple snippet below -
We can create a context with a default value. Of course, we need to import the React component library as usual. A typical component we could pass around is a color themed component for example:
Wrap whatever component you intend to pass data to, say `SampleComponent` in a provider (the `ColorContext` in this case), shown below:
And this is how you use the `SampleComponent` with the context instantiation below. Say the `SampleComponent` returns a Button, all we need to do is to just call the current context on that component and that is all. See below:
Note that even though we have another component wrapping the `SampleComponent`, we do not have to explicitly pass down the data anymore. React handles passing down the closest color provider above and then makes use of its current value.
While context is great, using context in lots of places in our application makes it hard to reuse our components in other places, since the component is already tied to that particular context. And as we may know, this defeats the ethos and selling point of React components being reusable across the entire stack.
Why do we use Context in React?
As we have earlier mentioned, we use context in React when we need to pass data down the component tree. As we may know, react is a component-based library for building declarative user interfaces. Since it is component based, there is a need to share data across the component tree.
Passing data from the topmost or top-level component to the bottommost or last component down the tree can be cumbersome and tedious for medium to large scale apps. Before context was introduced, we either had to use external packages chief of which was Redux or pass the data as props one component at a time down the tree, until we got to our target component down the tree. Of course, this would be impossible to do for exceptionally large apps. This is indeed what React context attempts to alleviate for us.
This data is usually global data in a store, which sort of acts like a source of truth for our data cache. A typical example would be an app that serves content in many different languages. Say I am a Finnish speaker and I switch to Finnish, I expect all the pages I visit to be in Finnish right?
For us to easily achieve this, we can just pass the props to the component that needs them only, which corresponds to the pages I am visiting and bypass all other intermediate components that do not care about this prop. Of course, this is the power of the context API.
Alternatively, if we only have the goal of not passing data or props across just a few portions of the component tree of our application, we can make use of component composition, which is a simpler version of what context does. Component composition is a React paradigm which advocates for using large containers to wrap or return smaller or children components. The idea is favoring component composition over component inheritance.
The only caveat with this approach is of course, keeping the top-level or higher-level component in the tree too complicated, since all the logic is happening there.
In this post, we will not be focusing much on this topic. For more details, do check out the react documentation, where it is extensively covered. However, with context, we can pass the same data to many components in the tree, and when any change occurs at the source, this change is equally propagated automatically down to every component level that has access to this data.
How does React Context Work?
React context works by exposing a ´context object´. As shown below, we declare a context object with a default value:
This context object, `MyContext` has a `Provider` component that allows as many other components as possible below it who need access to context object changes, subscribe to it. This means that context changes can be subscribed to on the component hierarchy by consuming components who must be children of Provider components. The signature of a Provider component is shown below:
It comes with a value prop, which is passed to the components down the tree that are descendants of the said Provider. This means that whenever the Provider props changes, these changes would also be propagated down the tree to every component subscribed to the Provider component, which would of course cause a re-render.
During render or in other lifecycle methods like componentDidMount(), componentDidUpdate() and so on, we can make use of the current value of the context. Let us see an example with the `render()` method below:
As you can see on the first-line, we are initializing our context object here, and assigning the `contextType` to the class that needs that context. In doing so, this allows us to consume the nearest context value using `this.context` inside any react lifecycle hook or even during render. Note that we can only subscribe to just a single context with this approach. Not to worry, we will look at an example later in the post.
Note: In the case where there is no matching Provider up in the tree, the component makes use of the ‘defaultValue' from the initial context object instantiation. This tells us that as many consumers as possible can subscribe to a single context object or Provider component, as the case may.
When should you use React Context?
Like we have highlighted earlier, react context shines when we need to pass data as props from a top-level global component to multiple other components down the hierarchy without having to explicitly pass the props to each other component. The point is, when we need access to data at a very deep level, it would be tedious to pass that data to every component, including those that dont need access to this data.
As a good example, consider we intend to get the current authenticated user on a web page, we can share that data across from a component at the global level to the protected route that needs access to that data. With context, we do not necessarily have to pass props through intermediate components that have no business knowing anything about the data.
How to use React Context (Creating your own context)
Now, let us learn how to make use of react context by creating our own context. We will be making use of the code sandbox playground to demonstrate our example. Supposing we plan to share language preference data from a global store state, first thing we need to do is declare the context object shown below -
We start by creating a file and calling it `Language.js`. In this file, we instantiate our context object:
At this point our home component is empty. Let us assume this component is a component down the tree hierarchy that needs access to this Language context and is subscribed to it. Let us define the component below:
Finally, we can now add our root component. Let us call it ìndex.js`. See below:
The output when we run the code on the sandbox is shown below:
A link to the sandbox can be found here. We can play with the values in the top-level component and see what happens. Also, note that we can as well define a new context type at any level and the consumer of that context would get the new value defined. Next up, let us look at the `useContext` hook, a React hook that replaces all the magic happening in the `Home` component file of our example.
React hooks was introduced in the React 16.8 version release to ditch state and other features of react hitherto only available when working with class components to now work with function components in the library. The signature of this React hook is shown below:
This is equivalent to calling:
static contextType = MyContext, in a class-based component as we saw earlier. And as we already know, this context hook accepts the context object as usual and returns the current context value for that context. More details about hooks in general can be found in the docs.
React context versus React Redux
As we have learnt in the past few sections, context was designed for passing data deep down in nested components in the component hierarchy or tree. This is unlike Redux. Of course, with Redux, we can achieve this, but even more. In fact, Redux uses the context API under the hood to pass data across multiple levels of the component hierarchy. As we may know, every tool comes with a tradeoff, and it is our duty as engineers to pick the right tools for the right job as the use case demands.
If the idea is to only perform the actions of what context does with Redux in our applications, then we might not need Redux anymore and can simply replace it with the inbuilt core context API. However, if we need other stuff that comes with Redux, including its powerful dev tools, ability to step through the state at every point in the component tree, redux middleware capabilities which allows us to do even much more when it comes to application logic, then we are in for a big day.
According to the documentation, Redux is a predictable state container for react applications, that centralizes state updates from a read only store which acts as the sole source of truth for your application data. Redux also comes with other third party associated libraries like `redux-persist` that allows you to store data in localstorage and perform rehydration magic on refresh. In fact redux has lots of use cases which makes it more powerful that context. We can learn more about react-redux from the documentation. To learn more about use cases of redux, do well to also check out this link.
Awesome use cases for context can be found in applying global stuff in our apps like theming, localization, login data, routing and so on. A detailed information about context can be found in the React documentation here. It provides a perfect resource to learn more about context, edge cases not covered here, more use cases and gotchas and more. Thanks for reading and cheers.