Real-world createContext

The new React Context API is coming. In 16.3, we will finally have a first-class alternative to the old React Context that we’ve been warned not to use. But is the new createContext worth it, or is it Yet Another Change that we have to migrate to?

Rather than go over the new API in-depth (there is already an amazing article on that by Kent C. Dodds), I’m going to investigate the createContext API with a more concrete example.

Auto updating descriptive times

In many applications I’ve made using React, I usually have some date time objects that I’d like to display as 1 minute ago, 2 hours ago, or 1 day ago. Usually, that’s easily implemented using moment and creating a simple <DateTime /> component:

import React from 'react';
import moment from 'moment';
const DateTime = ({ since, now = new Date() }) => (
<span>{moment(since).from(moment(now))}</span>
);
export default DateTime;

Now the problem becomes making this DateTime component update regularly so that something posted 1 minute ago will say 2 minutes ago once a minute passes.

A simple setInterval on the parent component will usually work, but if you have a large set of DateTime components on your page, you’ll soon notice that all of the setInterval calls will cause each of these components to re-render at sporadic intervals.

It’d be nice to make it so that every parent component that wanted to show a DateTime didn’t have to manage a setInterval , and it would be even nicer if all DateTime components could update at the same time.

Making a Context

We’re going to use the createContext API to create a context that will have a setInterval that will run and auto-update the current time every second. The createContext API will give us a Provider and Consumer. We can wrap the Provider and use that to update the value our context holds, and we use the Consumer to get that value.

Here’s what the TimeProvider component looks like:

import React, { createContext, Component } from 'react';const TimeContext = createContext({
time: new Date(),
});
export class TimeProvider extends Component {
state = {
time: new Date(),
}
componentDidMount() {
this.timer = setInterval(() => {
this.setState({
time: new Date(),
})
}, 1000);
}
componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
render() {
const { time } = this.state;
return (
<TimeContext.Provider value={this.state}>
{this.props.children}
</TimeContext.Provider>
);
}
}
export const TimeConsumer = TimeContext.Consumer;

The section const TimeContext = createContext({ ... }) defines a new context using the React API that has a Provider and a Consumer that we will use.

I then define a component that uses the TimeContext.Provider and sets a new value for the context by using React’s normal setState to have the component re-render and set the value.

I also export const TimeConsumer = TimeContext.Consumer; so that it hides the implementation details for components that want to consume this context.

Using the Consumer

When we use the new createContext API from React, we will have a Consumer and a Provider. Our consumer will use the data from our context and the provider will set the data in our context.

What I’ll do is create a <Clock /> component that will use a date given by the context. So our Clock component will just use a TimeConsumer component that we’ll define later:

import React from 'react';
import { TimeConsumer } from './TimeProvider';
import moment from 'moment';
const Clock = ({ since = new Date() }) => (
<TimeConsumer>
{({ time }) => (
<DateTime since={since} now={time} />;
)}
</TimeConsumer>
);
export default Clock;

The TimeConsumer will use our render-prop and have it pass in the latest Date object so that our DateTime component will render with the latest difference between our since date and the newest now date.

So when we use <Clock since={oneSecondAgo} /> we expect the result to be <span>1 second ago</span> and then in 1 second, it should say <span>2 seconds ago</span> . Similarly, if we had 20 Clock components on the page, we expect them all to re-render at the same time when 1 second passes.

Using the Provider

We have a component that uses the TimeConsumer component, but we need to make sure that we use the TimeProvider component somewhere.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import { TimeProvider } from './components/TimeProvider';
import Clock from './components/Clock';
class App extends Component {
render() {
return (
<TimeProvider>
<div><Clock since={new Date()} /></div>
<div><Clock since={moment(new Date()).add(-1, 'minute')} /></div>
<div><Clock since={moment(new Date()).add(-1, 'hour')} /></div>
<div><Clock since={moment(new Date()).add(-1, 'days')} /></div>
</TimeProvider>
);
}
}
var mountNode = document.getElementById("app");
ReactDOM.render(<App />, mountNode);

That’s it 🎉

You can see a working demo of the code above here.

I have also uploaded the code to Github, so you can check it out and play with it:

Using the new context API is straightforward and idiomatic. Using render-props is a nice way to avoid naming conflicts from different context providers. And the simple API lowers the barriers for creating and re-using context in web applications.

Additional Reading:

Ivan Montiel is the founder and CEO of Clarity Hub — a company that integrates with Intercom to give customer success teams real-time suggestions. He is also a UI Architect at Nextiva.

Twitter · Github · LinkedIn

UI Engineer at Lacework. Founder at Clarity Hub.