Instantly share code, notes, and snippets.

@deniztetik /blog.md Secret
Created Jul 8, 2018

Embed
What would you like to do?

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:

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.

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 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.

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.

https://gist.github.com/b25b334e23d31486c3e5cdd76e26abcb

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.

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).

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.

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.

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.
<iframe src="https://upscri.be/6e1a25?as_embed" height="400" frameborder="0" style="width:100%;max-width:800px;margin:0 auto;"></iframe>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment