From class components to function components with hooks

19 Feb, 2019
0 views
react

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.

AltStyle によって変換されたページ (->オリジナル) /