There's an interesting debate going on concerning where React Context leaves some pretty dominant third-party React libraries, particularly React Redux.
This article introducing Context by Marshall Zobel in particular caught my attention.
It's a great introduction to Context, and he very neatly lays out the primary use cases for it. For example, when you simply want to share data between nested components without constantly passing props down.
Yet he brings up an issue that I have seen in other places as well- namely that it does not provide the convenience that these aforementioned libraries do.
There seems to be an idea out there that Context is mostly suited to "read-only" situations. In my opinion, this is not the case at all.
In my previous post, I provided an example of how you could create a simple action where the Context Provider lives:
https://gist.github.com/d73087c9b830e5d58839a1d8bd4b0aef
Another argument is that asynchronous actions (API calls) are particularly problematic in this new paradigm.
What is one to do when there's a supcomponent somewhere that needs to receive information asynchronously on mount (for example, a sidebar or modal)? It may seem that there is no obvious way to do this.
A disclaimer here to review React Context if you're not familiar with how it works. The rest of the post assumes basic knowledge.
1.We want to create an app with a subcomponent somewhere that will update itself by making an asynchronous call on mount.
2.In addition, our root app component must house its own Context Provider. A corresponding Context Consumer will be placed in the subcomponent. From this subcomponent, we must be able to update on mount and update the state housed in the Context Provider.
3.We must also be able to access props in the entire component, not just in the render method. Since we typically wrap the Context Consumer in a render, this is not a trivial problem.
tldr; Update root's state after an asynchronous call on the subcomponent.
Let's fromt the end, shall we? 😬 To address problem #3, we will write a higher-order component (HOC) to wrap the subcomponent that will contain our Context Receiver. By doing this, we can elide the problem of only being able to access the context in the render statement.
Before we get ahead of ourselves, let's briefly review what a higher-order component is.
An HOC is a function that receives a component as an argument and returns another component (usually an enhanced version of the same component). For example, we may want to add some properties (but no side-effects!) that we defined which the returned component then will have access to.
Well-known examples of HOCs include the connect()
function from react-redux
and withRouter()
from react-router
.
As an example, the React docs provide a straightforward HOC that returns a component that logs its props. I provide it below, with a slight modification to bring it up to date to the current API.
https://gist.github.com/b25b334e23d31486c3e5cdd76e26abcb
Any component can then be enhanced with this logProps component: const EnhancedComponent = logProps(InputComponent);
.
Let's now get started with the solution. This is forked and modified from the Stackblitz React starter.
https://gist.github.com/e55c3ae1ba5989afb6e6e2fb61b31712
We create AppContext
and pass its Provider at the top level in our render. Our value
prop contains one state value name
and a method to modify our name (remember the value
prop is required for the Context Provider and can be any value).
Now before we get to consuming our Context, we need to create an HOC that will allow our Hello
component to consume it in its entirety. For convenience, we'll add it to the same file where our context is created.
https://gist.github.com/e064dacdb583dbdbd76982e1eda3ecf7
This is not a whole lot more complicated than the logProps
. A key difference here is that we are actually wrapping another component (AppContext.Consumer
) into the component that was passed in.
In the Consumer's child we have a function that adds all of the context as props to the Component, via the object spread operator. We then add any extra props that may have been passed directly into the Component.
With this done, our Hello
/Name
component can be created.
https://gist.github.com/09174255115303c6a33eb6855aa216df
By exporting Name
enhanced by withContext()
, that is the version that is rendered in index.js
.
Since we want to fetch some information on mount, we have an async componentDidMount()
, which makes our (fake) API call. Once we receive our response, we call the setName()
action that was passed into Context from the Provider.
This fulfills our conditions set out above.
We have a root component and a subcomponent that makes on update on mount (#1). Our App component wraps a Context Provider around its children, and our subcomponent (Hello/Message) uses a Context Consumer, via our HOC (#2). Last, we have access to our context wherever we like in Hello/Message, not just in .render()
(#3).
This is great, but I think we can do better. In my opinion, there is one main problem with the previous solution.
We are mixing API logic with UI concerns. It would be better to have separate areas in our app that can contain these kinds of logic.
Enter Local Context Containers (LCCs).
LCCs are about isolating components or group of components, and wrapping them into container components. Each container gets its own Context Provider. This allows us to group sections of our app into logical sections that should contain shared state.
This is not too different at all from the concept of Container Components in Redux.
This is particularly useful for things such as forms, or in general sections that serve as relatively isolated areas (navbar, modal, sidebar).
We now add a message container with its own Context that will hold our Hello/Name component.
https://gist.github.com/38fd0eb47d892abc75f0bb887251f20e
There is nothing conceptually new here. We are defining MessageContext
, MessageContainer
which provides that Context, and withMessageContext
which is an HOC that consumes our Context.
With this change, our Hello/Name component can be very small and clean. "Presentational," if you will 😁.
https://gist.github.com/dfb74a1dfdae841abacabc9abb42a6a2
Note, again, that we are exporting the enhanced Name component using withMessageContext()
.
Last, we update index.js
.
https://gist.github.com/8e27c46695786a223887881d60bb26d2
Nothing that different here, except for the fact that we are now using MessageContainer
.
Also, we keep AppContext. It's doing nothing right now (the value
prop in AppContext.Provider
is an empty object), but it could! This is merely to illustrate that we can have multiple contexts at the same time.
Well, that's about it! I hope you found this post a useful illustration of some concepts relating to Context. To cap:
- We can use higher-order components (HOCs) to wrap Context Consumers on any component. This is a nifty and reusable way to allow our entire components access specific Contexts without cluttering them up with Context logic.
- It's not too trivial to implement asynchronous actions using Context. If we want to fetch data asynchronously on mount or on change, there are several ways to do that.
- Local Context Containers (LCCs) is an approach to organizing React applications with Context. It allows us to cordon off different groups of components as children to components that consume the containers' Context.