Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

RFC: Update middleware #182

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"semi": [2, "never"],
"comma-dangle": ["error", "never"],
"no-use-before-define": ["error", { "functions": false }],
"no-param-reassign": ["error", { "props": false }],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-underscore-dangle": [0],
"no-confusing-arrow": [0],
Expand Down
22 changes: 10 additions & 12 deletions src/packages/recompose/__tests__/withPropsOnChange-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import sinon from 'sinon'
test('withPropsOnChange maps subset of owner props to child props', t => {
const mapSpy = sinon.spy()
const StringConcat = compose(
withState('strings', 'updateStrings', { a: 'a', b: 'b', c: 'c' }),
withState('strings', 'updateStrings', { a: 'a', b: 'b', c: 'c', d: 'd' }),
flattenProp('strings'),
withPropsOnChange(
['a', 'b'],
({ a, b, ...props }) => {
['c', 'd'],
({ c, d }) => {
mapSpy()
return {
...props,
foobar: a + b
derived: c + d
}
}
)
Expand All @@ -29,17 +28,16 @@ test('withPropsOnChange maps subset of owner props to child props', t => {
const div = mount(<StringConcat />).find('div')
const { updateStrings } = div.props()

t.is(div.prop('foobar'), 'ab')
t.is(div.prop('derived'), 'cd')
t.is(mapSpy.callCount, 1)

// Does not re-map for non-dependent prop updates
updateStrings(strings => ({ ...strings, c: 'baz' }))
t.is(div.prop('foobar'), 'ab')
t.is(div.prop('c'), 'c')
updateStrings(strings => ({ ...strings, a: 'a2' }))
t.is(div.prop('a'), 'a2')
t.is(div.prop('derived'), 'cd')
t.is(mapSpy.callCount, 1)

updateStrings(strings => ({ ...strings, a: 'foo', b: 'bar' }))
t.is(div.prop('foobar'), 'foobar')
t.is(div.prop('c'), 'baz')
updateStrings(strings => ({ ...strings, c: 'c2', d: 'd2' }))
t.is(div.prop('derived'), 'c2d2')
t.is(mapSpy.callCount, 2)
})
4 changes: 2 additions & 2 deletions src/packages/recompose/createHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const createHelper = (
`like so: ${helperName}(...args)(BaseComponent).`
)
}

const hoc = func(...args)
return BaseComponent => {
const Component = func(...args)(BaseComponent)
const Component = hoc(BaseComponent)
Component.displayName = wrapDisplayName(BaseComponent, helperName)
return Component
}
Expand Down
16 changes: 6 additions & 10 deletions src/packages/recompose/flattenProp.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import mapProps from './mapProps'
import omit from './utils/omit'
import createHelper from './createHelper'
import createEagerFactory from './createEagerFactory'

const flattenProp = propName => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
return props => (
factory({
...omit(props, [propName]),
...props[propName]
})
)
}
const flattenProp = propName =>
mapProps(props => ({
...omit(props, [propName]),
...props[propName]
}))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case it's not clear why this is included in this PR (other than being the right way to define flattenProp, anyway), the idea is that there are some helpers that we don't want to define using middleware, because middleware necessitates using a class component, even when a functional component would suffice. What we really want is to use a functional component unless the HOC is composed with another HOC which must use a class. Put another way, a single functional component is better than a class component, but a single class component is better than a class component and a functional component.

To achieve this, we will special case mapProps to use a functional component by default, but use middleware when being composed with other middleware-using HOCs.


export default createHelper(flattenProp, 'flattenProp')
2 changes: 0 additions & 2 deletions src/packages/recompose/renameProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ const { keys } = Object
const mapKeys = (obj, func) =>
keys(obj).reduce((result, key) => {
const val = obj[key]
/* eslint-disable no-param-reassign */
result[func(val, key)] = val
/* eslint-enable no-param-reassign */
return result
}, {})

Expand Down
2 changes: 0 additions & 2 deletions src/packages/recompose/setStatic.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import createHelper from './createHelper'

const setStatic = (key, value) => BaseComponent => {
/* eslint-disable no-param-reassign */
BaseComponent[key] = value
/* eslint-enable no-param-reassign */
return BaseComponent
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react'
import test from 'ava'
import { mount } from 'enzyme'
import createHocFromMiddleware from '../createHocFromMiddleware'
import { compose } from '../../'

const m1 = ({ getProps }) => next => ({
update: ({ foo, ...rest }) => {
const currentProps = getProps()
next.update({
...rest,
currentFoo: foo,
previousFoo: currentProps && currentProps.foo
})
}
})

const m2 = ({ getProps }) => next => ({
update: ({ bar, ...rest }) => {
const currentProps = getProps()
next.update({
...rest,
currentBar: bar,
previousBar: currentProps && currentProps.bar
})
}
})

const hoc1 = createHocFromMiddleware(m1)
const hoc2 = createHocFromMiddleware(m2)

// Does not use middleware, so will create an extra class
const identityHoc = BaseComponent =>
class extends React.Component {
render() {
return <BaseComponent {...this.props} />
}
}

const getNumberOfComponents = wrapper =>
wrapper.findWhere(w => typeof w.type() !== 'string').length

const assertHoc = (t, testHoc, expectedNumberOfComponents) => {
const Div = testHoc('div')
const wrapper = mount(<Div foo={1} bar={2} />)
const div = wrapper.find('div')

t.is(div.prop('currentFoo'), 1)
t.is(div.prop('previousFoo'), undefined)
t.is(div.prop('currentBar'), 2)
t.is(div.prop('previousBar'), undefined)

wrapper.setProps({ foo: 2, bar: 3 })
t.is(div.prop('currentFoo'), 2)
t.is(div.prop('previousFoo'), 1)
t.is(div.prop('currentBar'), 3)
t.is(div.prop('previousBar'), 2)

const numberOfComponents = getNumberOfComponents(wrapper)
t.is(numberOfComponents, expectedNumberOfComponents)

return wrapper
}

test('createHocFromMiddleware gives access to current props', t => {
assertHoc(t, createHocFromMiddleware(m1, m2), 1)
})

test('createHocFromMiddleware squashes with middleware from base component', t => {
assertHoc(t, compose(hoc1, hoc2), 1)
assertHoc(t, compose(hoc1, hoc2, identityHoc), 2)
assertHoc(t, compose(hoc1, identityHoc, hoc2), 3)
assertHoc(t, compose(identityHoc, hoc1, hoc2), 2)
})
96 changes: 96 additions & 0 deletions src/packages/recompose/utils/createHocFromMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Component } from 'react'
import createEagerFactory from '../createEagerFactory'

const MIDDLEWARE_INFO = typeof Symbol === 'function'
? Symbol()
: '@@recompose/middlewareInfo'

const createComponentFromMiddleware = (middlewares, BaseComponent) => {
const factory = createEagerFactory(BaseComponent)
class CreateComponentFromMiddleware extends Component {
constructor(initialProps) {
super(initialProps)
this.pendingXformProps = [initialProps]
this.xformProps = []

this.xforms = middlewares.reduce((xforms, middleware, i) => {
const createXform = middleware({
getProps: () => this.xformProps[i]
})
xforms[i] = createXform({
update: (props, cb) => {
this.xformProps[i] = this.pendingXformProps[i]
this.pendingXformProps[i + 1] = props
if (i === middlewares.length - 1) {
this.applyPropUpdate(props, cb)
} else {
this.xforms[i + 1].update(props, cb)
}
}
})
return xforms
}, [])
}

state = { childProps: null }
_isMounted = false
didReceivePropUpdate = false

destroy() {
this.xforms.forEach(xform => {
if (typeof xform.destroy === 'function') {
xform.destroy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the only need the middleware returns object is .destroy?

}
})
}

applyPropUpdate(props, cb) {
if (!this._isMounted) {
return
}
this.didReceivePropUpdate = true
this.setState({
childProps: props
}, cb)
}

componentWillMount() {
this._isMounted = true
this.xforms[0].update(this.pendingXformProps[0])
}

componentWillReceiveProps(nextProps) {
this.pendingXformProps[0] = nextProps
this.xforms[0].update(nextProps)
}

componentWillUnmount() {
this._isMounted = false
this.destroy()
}

render() {
if (!this.didReceivePropUpdate) {
return null
}
return factory(this.state.childProps)
}
}
CreateComponentFromMiddleware[MIDDLEWARE_INFO] = {
middlewares,
BaseComponent
}
return CreateComponentFromMiddleware
}

const createHocFromMiddleware = (...middlewares) => BaseComponent => {
if (BaseComponent[MIDDLEWARE_INFO]) {
return createComponentFromMiddleware(
middlewares.concat(BaseComponent[MIDDLEWARE_INFO].middlewares),
BaseComponent[MIDDLEWARE_INFO].BaseComponent
)
}
return createComponentFromMiddleware(middlewares, BaseComponent)
}

export default createHocFromMiddleware
42 changes: 19 additions & 23 deletions src/packages/recompose/withHandlers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component } from 'react'
import createEagerFactory from './createEagerFactory'
import createHocFromMiddleware from './utils/createHocFromMiddleware'
import createHelper from './createHelper'

const mapValues = (obj, func) => {
Expand All @@ -16,21 +15,20 @@ const mapValues = (obj, func) => {
return result
}

const withHandlers = handlers => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
return class extends Component {
cachedHandlers = {};
const withHandlers = handlers =>
createHocFromMiddleware(({ getProps }) => next => {
let cachedHandlers = {}

handlers = mapValues(
const handlerProps = mapValues(
handlers,
(createHandler, handlerName) => (...args) => {
const cachedHandler = this.cachedHandlers[handlerName]
const cachedHandler = cachedHandlers[handlerName]
if (cachedHandler) {
return cachedHandler(...args)
}

const handler = createHandler(this.props)
this.cachedHandlers[handlerName] = handler
const handler = createHandler(getProps())
cachedHandlers[handlerName] = handler

if (
process.env.NODE_ENV !== 'production' &&
Expand All @@ -44,19 +42,17 @@ const withHandlers = handlers => BaseComponent => {

return handler(...args)
}
);

componentWillReceiveProps() {
this.cachedHandlers = {}
}

render() {
return factory({
...this.props,
...this.handlers
})
)

return {
update: (nextProps, cb) => {
cachedHandlers = {}
next.update({
...nextProps,
...handlerProps
}, cb)
}
}
}
}
})

export default createHelper(withHandlers, 'withHandlers')
33 changes: 16 additions & 17 deletions src/packages/recompose/withPropsOnChange.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { Component } from 'react'
import pick from './utils/pick'
import shallowEqual from './shallowEqual'
import createHelper from './createHelper'
import createEagerFactory from './createEagerFactory'
import createHocFromMiddleware from './utils/createHocFromMiddleware'

const withPropsOnChange = (shouldMapOrKeys, propsMapper) => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
const withPropsOnChange = (shouldMapOrKeys, propsMapper) => {
const shouldMap = typeof shouldMapOrKeys === 'function'
? shouldMapOrKeys
: (props, nextProps) => !shallowEqual(
pick(props, shouldMapOrKeys),
pick(nextProps, shouldMapOrKeys),
)

return class extends Component {
computedProps = propsMapper(this.props);
return createHocFromMiddleware(({ getProps }) => next => {
let didUpdate = false
let computedProps = null

componentWillReceiveProps(nextProps) {
if (shouldMap(this.props, nextProps)) {
this.computedProps = propsMapper(nextProps)
return {
update: (nextProps, cb) => {
if (!didUpdate || shouldMap(getProps(), nextProps)) {
didUpdate = true
computedProps = propsMapper(nextProps)
}
next.update({
...nextProps,
...computedProps
}, cb)
}
}

render() {
return factory({
...this.props,
...this.computedProps
})
}
}
})
}

export default createHelper(withPropsOnChange, 'withPropsOnChange')
Loading