Introduction
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:
// ./src/App.js
import React, { Component } from 'react';
import './App.css';
import Post from './Post';
import EditForm from './EditForm';
import { AppContext, context } from './app-context';
class App extends Component {
/* .addUpVote() and .savePost() are added to the state object which is
then added to the Context Provider's value prop */
addUpVote = () => {
this.setState({
upvotes: this.state.upvotes + 1,
});
}
savePost = (post) => {
const {
title,
content,
} = post;
this.setState({
title,
content,
})
}
state = {
title: context.title,
content: context.content,
savePost: this.savePost,
upvotes: context.upvotes,
addUpVote: this.addUpVote,
}
render() {
return (
<AppContext.Provider value={this.state}>
<div className="main">
<Post />
<EditForm />
</div>
</AppContext.Provider>
);
}
}
export default App;
Another argument is that asynchronous actions (API calls) are particularly problematic in this new paradigm.
What is one to do when there’s a subcomponent 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.
The Problem
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.
The Solution
Let’s from 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.
HOC?
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.
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Previous props: ', prevProps);
console.log('Current props: ', this.props);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
Any component can then be enhanced with this logProps component: const EnhancedComponent = logProps(InputComponent);
.
The App
Let’s now get started with the solution. This is forked and modified from the Stackblitz React starter.
// ./src/index.js
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';
const AppContext = React.createContext();
class App extends Component {
state = {
name: 'Angular',
}
render() {
return (
<AppContext.Provider value={{
name: this.state.name,
setName: (name) => this.setState({ name }),
}}>
<div>
<Hello />
<p>
Start editing to see some magic happen :)
</p>
</div>
</AppContext.Provider>
);
}
}
render(<App />, document.getElementById('root'));
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.
// ./src/index.js
// imports
const AppContext = React.createContext();
export function withContext(Component){
return (props) => (
<AppContext.Consumer>
{contextProps =>
/* override contextprops with specific name
if prop of same name is manually added */
<Component {...contextProps } {...props} />
}
</AppContext.Consumer>
);
}
// App component
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.
// ./src/Hello.js
import React, { Component } from 'react';
import { withContext } from './index';
class Name extends Component {
async componentDidMount() {
const name = await delay('React', 0);
this.props.setName(name);
}
render() {
return <h1>Hello {this.props.name}!</h1>;
}
}
export default withContext(Name);
// simulate an API Call
const delay = (value, time = 0) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
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).
Going Further – Local Context Containers
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).
Adding a Message Container
We now add a message container with its own Context that will hold our Hello/Name component.
// ./src/MessageContainer.js
import React, { Component } from 'react';
import Name from './Hello';
const MessageContext = React.createContext();
class MessageContainer extends Component {
state = {
name: 'Angular',
setName: this.setName,
}
async componentDidMount() {
await this.setName('React');
}
/* Put actions here */
async setName(name) {
const nameResponse = await delay(name, 0);
this.setState({ name: nameResponse });
}
render() {
return (
<MessageContext.Provider value={this.state}>
<Name />
</MessageContext.Provider>
);
}
}
export default MessageContainer;
export function withMessageContext(Component) {
return (props) => (
<MessageContext.Consumer>
{contextProps =>
/* override contextprops if prop of same name is manually added */
<Component {...contextProps} {...props} />
}
</MessageContext.Consumer>
);
}
// simulate an API Call
const delay = (value, time = 0) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
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 ?.
// ./src/Hello.js
import React from 'react';
import { withMessageContext } from './MessageContainer';
const Name = (props) => (
<h1>Hello {props.name}!</h1>
);
export default withMessageContext(Name);
Note, again, that we are exporting the enhanced Name component using withMessageContext()
.
Last, we update index.js
.
// ./src/index.js
import React, { Component } from 'react';
import { render } from 'react-dom';
import MessageContainer from './MessageContainer';
import './style.css';
const AppContext = React.createContext();
export function withContext(Component) {
return (props) => (
<AppContext.Consumer>
{contextProps =>
/* override contextprops if prop of same name is manually added */
<Component {...contextProps} {...props} />
}
</AppContext.Consumer>
);
}
class App extends Component {
render() {
return (
<AppContext.Provider value={{}}>
<MessageContainer />
</AppContext.Provider>
);
}
}
render(<App />, document.getElementById('root'));
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.
Wrapping Up
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.
P.S. The repo containing some of the examples found in this blog post is here.
Recent Comments