Bitmasks and the new React Context API
Updated on 19 May 2018
It appears that the observedBits
prop to the Consumer
component, described below, has now been changed to unstable_observedBits
. The linked code sample has now been updated to use the new prop, but please keep in mind that this API is subject to change.
If you’ve used React, and you’ve spent some time browsing its documentation, you may have come across the section on the Context API which starts by discouraging its use altogether, saying that it is experimental and subject to change in the future. This is particularly disheartening because context can solve some common problems quite conveniently, such as providing application state down a deeply nested tree and building interdependent or compound components that would otherwise require their user to manually control them.
Finally, the time has come — the release of React 16.3.0 is imminent and it includes a new and fully sanctioned Context API. I’m especially excited about this update due to its potential to positively impact how React applications and components are built.
In this post, I will cover all the essential parts of the new API along with an interesting escape hatch that it provides for performance optimization.
The new Context API
Andrew Clark, a core member of the React team at Facebook, recently introduced a proposal for a new Context API. The proposal was quickly accepted and has now been implemented and merged, and it will be included as part of the next minor version update of React. Conveniently, it has already been released under the next
tag, which makes it publicly available for anyone to try it out:
yarn add react@next react-dom@next
The following should demonstrate the absolute basics of the new API (see interactive demo):
import React from 'react';
import { render } from 'react-dom';
const { Consumer, Provider } = React.createContext();
render(
<Provider value="Hello, world!">
<div>
<Consumer>{value => <p>{value}</p>}</Consumer>
</div>
</Provider>,
document.getElementById('root'),
);
In the above example, we’ve created two components with React.createContext
. The resulting Provider
component makes any value it is given, accessible to any and all instances of the associated Consumer
component. The div
between the two components is not required; it is only there to demonstrate that there’s no direct parent–child relationship between them for the sake of supplying the data.
Providing application state
Generally speaking, it’s not good practice to make all components rely directly on a global state store, since highly coupled code is harder to extend, refactor, and test. Thankfully, we can address this by creating regular components (i.e., standard prop–based rather than context–based components) and then wrap them in order to provide them with access to any data that they may need.
We’ll start by creating a higher-order component that uses the same Consumer
that we created earlier:
const withState = WrappedComponent => props => (
<Consumer>
{state => <WrappedComponent {...state} {...props} />}
</Consumer>
);
We can now use this function to wrap any components that require access to application state. For instance, let’s create a couple of components that respectively rely on a user
object and a films
array:
const Welcome = ({ user }) => (
<p>Hello, {user.name}!</p>
);
const FavouriteFilms = ({ films }) => (
<div>
<p>Some of your favourite films are:</p>
<ul>
{films.map(film => (
<li key={film.name}>{film.name}</li>
)}
</ul>
</div>
);
// Wrap the components separately to give access to both variants.
const WelcomeWithState = withState(Welcome);
const FavouriteFilmsWithState = withState(FavouriteFilms);
With the components defined, we can now define our application state and render them:
const state = {
user: {
name: 'Hawk',
},
films: [
{
name: 'There Will Be Blood',
},
{
name: 'Apocalypse Now',
},
],
};
// What follows is equivalent to passing props to the
// original components manually, that is:
// <Welcome user={state.user} />
// <FavouriteFilms films={state.films} />
render(
<Provider value={state}>
<WelcomeWithState />
<FavouriteFilmsWithState />
</Provider>,
document.getElementById('root'),
);
The above may not seem all that useful, but in a growing or large application this approach will quickly start to pay off. However, this example lacks one crucial aspect of a real-world app: the data is static. We can provide a way to update the state by wrapping the Provider
component in its own provider of sorts, and update it with good old state
.
class StateProvider extends React.Component {
state = {
...this.props.initialState,
setGlobalState: this.setState.bind(this),
};
render () {
return (
<Provider value={this.state}>
{this.props.children}
</Provider>
);
}
}
render(
<Provider value={state}>
<StateProvider initialState={initialState}>
<WelcomeWithState />
<FavouriteFilmsWithState />
</Provider>,
</StateProvider>
document.getElementById('root'),
);
Any descendants of StateProvider
can now make use of setGlobalState
, which is available as a prop, to update the state tree. To see an example of this, check out this demo which includes an input and a button to update the list of favourite films.
What about performance?
In a large and complex React application, it is important to prevent unnecessary rerenders. As you may have guessed, all the instances of Consumer
will rerender unless explicitly told not to. In order to implement something more akin to the publish-subscribe pattern, where subscribers (or here, consumers) only receive the slice of the state that they subscribe to, we must provide a way for React to know whether to update the component or not.
This is where bitmasks come in. The new React.createContext
takes a function as an optional second argument. This function, referred to internally as calculateChangedBits
, is called by the associated Provider
every time its value changes. The function receives the current and the next value as arguments, and this can be used to create a bitmask. Instances of the Consumer
component must then be provided with an observedBits
prop, which will determine whether the component needs to be updated or not.
As a simple (and perhaps rather contrived) example, consider a UI that has a single number as its state and updates every second to display the current value, the last odd number, and the last even number. The current value should be updated every second, while the other two should only be updated when the current tick of the value is either even or odd. Although the performance implications in this particular example are negligible, it provides a good starting point to understand how the context API employs bitmasks:
const calculateChangedBits = (currentValue, nextValue) =>
nextValue.value % 2 === 0 ? 0b10 : 0b1;
The above function will always return 0b1
or 0b10
, since all the numbers that we will be dealing with are either even or odd. Using this, we can create a new context and provide the appropriate observedBits
prop to our consumers (see the full demo here):
const { Consumer, Provider } = React.createContext(
null,
calculateChangedBits,
);
// Counter component, interval updates and changes to
// withState omitted; see the linked demo for a full example.
render(
<StateProvider initialState={{ value: 0 }}>
<Counter label="Current value" observedBits={0b11} />
<Counter label="Odd" observedBits={0b1} />
<Counter label="Even" observedBits={0b10} />
</StateProvider>,
document.getElementById("root"),
);
In the above example, with the omitted parts included, the first Counter
would be rerendered every time, since both 0b1
and 0b10
are “observed bits” in 0b11
. The second will only render when our calculateChangedBits
function returns 0b1
, and the third when it returns 0b10
. To illustrate how this works, consider the following example using the bitwise AND operator, where a non–zero return value means that the bit was set (you can verify this in your browser’s console):
// With a calculateChangedBits result of 0b1,
// the following cases are true:
(0b1 & 0b1) === 0b1
(0b10 & 0b1) !== 0b1
(0b11 & 0b1) === 0b1
// With a calculateChangedBits result of 0b10,
// the following cases are true:
(0b1 & 0b10) !== 0b10
(0b10 & 0b10) === 0b10
(0b11 & 0b10) === 0b10
Ostensibly, the reason for which bitmasks were chosen for this purpose is that they can efficiently encode the boolean state of which child consumers should be updated with a single function call. Most of the time, this feature will only be used by libraries such as Redux, MobX, styling libraries, and so forth, but it’s good to know of its existence in case you need it.
What does all this mean for the future of React?
As with all new things, only time will tell whether the new API is more successful and popular than the previous one. However, given that the old version was heavily discouraged by the React team itself, and considering the more expressive and powerful API, we’re likely to see some interesting experiments. I’d encourage you to see for yourself and to try it out next time you work on a new feature or project.
← All postsIf you’ve enjoyed reading this post, you can follow me on Twitter for updates.