sonhanguyen
9/6/2018 - 8:19 AM

Services in React's new context API

Services in React's new context API

import * as React from 'react'
import { render } from 'react-dom'
import { compose, fromRenderProps } from 'recompose' 
import { identity, pick, mapValues } from 'lodash'

const withDependencies = (...services: Array<[any, Function]>) => {
  const enhance = compose(
    ...services.map(([service, mapProps]) => fromRenderProps(
      Service.contextFor(service).Consumer,
      mapProps
    ))
  )

  return component => Object.assign(enhance(component), {
    [Service.TAG]: component
  })
}

class Service extends React.Component<{ children: React.ReactNode }> {
  private provider = Service.contextFor(this.constructor).Provider
  private lastState = {}
  private lastContext = {}

  getContext() {
    if (this.lastState != this.state) {
      const actionKeys = Object.keys(this.constructor.prototype)
      actionKeys.shift() // ignore "constructor", which is the first key
      const actions = mapValues(
        pick(this, actionKeys),
        action => action.bind(this)
      )

      this.lastState = this.state
      this.lastContext = { ...actions, ...this.state }
    }

    return this.lastContext
  }

  render() {
    return <this.provider value={this.getContext()}>{this.props.children}</this.provider>
  }
}

namespace Service {
  export const TAG = Symbol()
  const register: Array<[React.Context<{}>, Context]> = []
  export const contextFor = service => {
    let [context] = register
      .find(([, it]) => [it, it[TAG]].includes(service)) || []
    
    if (!context) register.push([
      context = React.createContext({}),
      service
    ])

    return context
  }
}

class AStore extends Service {
  state = {text: 'a thing'}

  update() {
    this.setState({ text: 'the thing has changed' })
  }
}

@withDependencies(
  [AStore, identity]
)
class AService extends Service {
  updateStoreViaFacade() {
    this.props.update()
  }
}

const Test = withDependencies(
  [AService, identity],
  [AStore, context => ({ fromStore: context.text })],
)(props =>
  <div>
    {props.fromStore}
    <button onClick={props.updateStoreViaFacade}>TEST</button>
  </div>
)

function App() {
  // if services take dependencies on each other the order of wrapping should reflect
  return <AStore>
    <AService>
      <Test />
    </AService>
  </AStore>
}

render(<App />, document.body)