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)