I've been using React 16.5.2 context instead of Redux in the super secret app I'm working on, and I appreciate the simplicity improvement. The learning curve is pretty minimal, and there are fewer levels of abstraction. The one thing that was annoying me was the non-optimal ergonomics of the API when consuming the context.
The first thing that was annoying was the standard consumer wrap:
import React, { Component, createContext } from 'react'
const timerContext = createContext()
class Provider extends Component {
constructor() {
super()
this.state = {
store: {
active: false,
stop: this.stop.bind(this)
},
}
}
start() {
let { store } = this.state
store = { ...store, active: true }
this.setState({ store })
}
stop() {
let { store } = this.state
store = { ...store, active: false }
this.setState({ store })
}
render() {
return (
<timerContext.Provider value={this.state}>
{this.props.children}
</timerContext.Provider>
)
}
}
class Timer extends Component {
constructor () {
super()
this.state = {
activeTab: 'timer'
}
}
buttonClick (event) {
const { store } = this.props.context
if (store.active) {
store.stop()
} else {
store.start()
}
event.stopPropagation()
}
changeTab (value) {
this.setState({ activeTab: value })
}
render () {
const { store } = this.props.context
return (
<div>
<ul>
<li active={this.state.activeTab === 'timer'}
onClick={this.changeTab.bind(this, 'timer')}>
Timer
</li>
<li active={this.state.activeTab === 'manual'}
onClick={this.changeTab.bind(this, 'manual')}>
Manual
</li>
</ul>
<button active={store.active}
onClick={(event) => { this.buttonClick(event) }}>
{store.active ? 'Stop' : 'Start' }
</button>
</div>
)
}
}
// Not an actual layout, provider and consumer
// are not that near each other.
class Layout extends Component {
render() {
return (
<Provider>
{ /* This is the annoying part. */ }
<timerContext.Consumer>
{({ store }) => (
<Timer store={store}/>
)}
</timerContext.Consumer>
</Provider>
)
}
}
The second thing that was annoying was the fact that I had to wrap the consumer wrap in a higher-order component, to be able to use the context outside a render:
import React, { Component, createContext } from 'react'
// Our good old context.
const timerContext = createContext()
class Provider extends Component {
constructor() {
super()
this.state = {
store: {
active: false,
stop: this.stop.bind(this)
},
}
}
start() {
let { store } = this.state
store = { ...store, active: true }
this.setState({ store })
}
stop() {
let { store } = this.state
store = { ...store, active: false }
this.setState({ store })
}
render() {
return (
<timerContext.Provider value={this.state}>
{this.props.children}
</timerContext.Provider>
)
}
}
class Timer extends Component {
constructor () {
super()
this.state = {
activeTab: 'timer'
}
}
buttonClick (event) {
const { store } = this.props.context
if (store.active) {
store.stop()
} else {
store.start()
}
event.stopPropagation()
}
changeTab (value) {
this.setState({ activeTab: value })
}
render () {
const { store } = this.props.context
return (
<div>
<ul>
<li active={this.state.activeTab === 'timer'}
onClick={this.changeTab.bind(this, 'timer')}>
Timer
</li>
<li active={this.state.activeTab === 'manual'}
onClick={this.changeTab.bind(this, 'manual')}>
Manual
</li>
</ul>
<button active={store.active}
onClick={(event) => { this.buttonClick(event) }}>
{store.active ? 'Stop' : 'Start' }
</button>
</div>
)
}
}
// HOC we're introducing just to
// allow usage of context outside of render.
// Not great.
const withContext = (Component) => {
return (props) => (
<timerContext.Consumer>
{(context) => (
<Component {...props} context={context}/>
)}
</timerContext.Consumer>
)
}
// The Timer component is wrapped in HOC now.
const TimerWithContext = withContext(class Timer extends Component {
constructor () {
super()
this.state = {
activeTab: 'timer'
}
}
buttonClick (event) {
const { store } = this.props.context
if (store.active) {
store.stop()
} else {
store.start()
}
event.stopPropagation()
}
changeTab (value) {
this.setState({ activeTab: value })
}
render () {
const { store } = this.props.context
return (
<div>
<ul>
<li active={this.state.activeTab === 'timer'}
onClick={this.changeTab.bind(this, 'timer')}>
Timer
</li>
<li active={this.state.activeTab === 'manual'}
onClick={this.changeTab.bind(this, 'manual')}>
Manual
</li>
</ul>
<button active={store.active}
onClick={(event) => { this.buttonClick(event) }}>
{store.active ? 'Stop' : 'Start' }
</button>
</div>
)
}
})
class Layout extends Component {
render() {
return (
<Provider>
{ /* No longer that annoying, but we've introduced HOC. */ }
<TimerWithContext />
</Provider>
)
}
}
I've managed to get rid of those annoyances by upgrading to the latest React16.8.2, converting my class components to function components and replacing my nasty HOC with a simple hook.
Here's the result:
import React, { useContext, useState, Component, createContext } from 'react'
// The context yet again.
// Boring.
const timerContext = createContext()
class Provider extends Component {
constructor() {
super()
this.state = {
store: {
active: false,
stop: this.stop.bind(this)
},
}
}
start() {
let { store } = this.state
store = { ...store, active: true }
this.setState({ store })
}
stop() {
let { store } = this.state
store = { ...store, active: false }
this.setState({ store })
}
render() {
return (
<timerContext.Provider value={this.state}>
{this.props.children}
</timerContext.Provider>
)
}
}
// The Timer class has been converted to function.
function Timer (props) {
const { store } = useContext(timerContext)
const [tab, setTab] = useState('timer')
const buttonClick = (event) => {
if (store.active) {
store.stop()
} else {
store.start()
}
event.stopPropagation()
}
return (
<div>
<ul>
<li active={tab === 'timer'}
onClick={() => setTab('timer')}>
Timer
</li>
<li active={tab === 'manual'}
onClick={() => setTab('manual')}>
Manual
</li>
</ul>
<button active={store.active}
onClick={(event) => { buttonClick(event) }}>
{store.active ? 'Stop' : 'Start' }
</button>
</div>
)
}
class Layout extends Component {
render() {
return (
<Provider>
{ /* Very slick now */ }
<Timer />
</Provider>
)
}
}
Nothing is better than removing code, so this is a win. Upgrading to a newer version of React was worth it.