I noticed that many of them have a very similar behavior, and I end up having to repeat a lot of code for each one of them. I am always apprehensive when it comes to abstractions, but I believe that in many cases it is extremely useful. My idea is to create a helper to create these State Container components.
E.g.:
const Toggle = createStateContainer({
displayName: 'Toggle',
initialState: false,
renderProps: ({ state, setState }) => ({
on: state,
toggle: () => setState(state => !state)
})
})
const complement = fn => (...args) => !fn(...args)
const List = createStateContainer({
displayName: 'List',
initialState: [],
renderProps: ({ state, setState }) => ({
list: state,
first: () => state[0],
last: () => state[Math.max(0, state.length - 1)],
push: (value) => setState(s => ([ ...s, value ])),
reject: (fn) => setState(s => s.filter(complement(fn))),
filter: (fn) => setState(s => s.filter(fn)),
map: (fn) => setState(s => s.map(fn)),
})
})
Even within more complex cases:
const hasItem = arr => item => arr.indexOf(item) !== -1
const removeItem = item => arr => hasItem(arr)(item) ? arr.filter(d => d !== item) : arr
const addUnique = item => arr => hasItem(arr)(item) ? arr : [...arr, item]
const Set = createStateContainer({
displayName: 'Set',
initialState: [],
renderProps: ({ state, setState }) => ({
values: state,
add: item => setState(addUnique(item)),
remove: item => setState(removeItem(item)),
has: hasItem(state),
})
})
// This Mouse component does not yet exist in powerplug but it is a very old wish
// I've used as complex example.
// This is a raw implementation: https://codesandbox.io/s/vnx11ymv67 (with some differences)
const Mouse = createStateContainer({
displayName: 'Mouse',
initialState: {
x: null,
y: null,
ratioX: null,
ratioY: null,
pageX: null,
pageY: null
},
renderProps: ({ setState }, { ratioFix /* props passed to <Mouse> */}) => ({
bind: {
onMouseEnter: (event) => {
// cache for better performance
this.boundingClientRect = event.currentTarget.getBoundingClientRect()
},
onMouseMove: (event) => {
const { left, top, width, height } = this.boundingClientRect
const { clientX, clientY, pageX, pageY } = event
const x = (clientX - left + 1).toFixed(3)
const y = (clientY - top + 1).toFixed(3)
const useRatioFix = typeof ratioFix !== 'undefined' ? ratioFix : 3
const ratioX = Math.min(1, x / width).toFixed(useRatioFix)
const ratioY = Math.min(1, y / height).toFixed(useRatioFix)
setState({ x, y, pageX, pageY, ratioX, ratioY })
}
}
})
})
In this implementation, I overwritten the setState to accept all types (boolean, string, number, array, object, etc), so we can simplify it even more.
Things to note:
- All components will have
state
and setState
for its primary purpose. For example, state
for Toggle will be the boolean value of "on" or "off". In this case we can use the Toggle method toggle
or if need, setState
directly. Same for all other components.
<Toggle initial={false}>
{({ state, setState, on, toggle }) => ()}
</Toggle>
<Mouse>
{({ state, setState, bind }) => <div {...bind}>{state.ratioX}</div>}
</Mouse>
// etc
I think we can gain some benefits like:
- Standardize all the components. So all will have the same behavior. (I could easily type some components with a 'StateContainer' generic type in TypeScript)
- No more code repetition, like
onChange
binding, renderProps util, etc.
- We can export this createStateContainer thing, so users can create their own state containers.
And finally, here is a draft of the implementation:
import * as React from 'react'
import renderPropsUtil from './renderProps'
const isFn = arg => typeof arg === 'function'
const cbs = (...fns) => (...args) => fns.forEach(fn => isFn(fn) && fn(...args))
export const createStateContainer = ({
initialState,
renderProps,
displayName,
}) => {
return class StateContainer extends React.Component {
static displayName = displayName || 'StateContainer'
// We use `value` as main state for all components
// so we can save simple values that are not objects
state = {
value:
typeof this.props.initial === 'undefined'
? initialState
: this.props.initial,
}
setStateValue = (setter, cb) =>
this.setState(
({ value }) => ({ value: isFn(setter) ? setter(value) : setter }),
cbs(cb, this.props.onChange)
)
stateHandler = () => ({
state: this.state.value,
setState: this.setStateValue,
})
// Bind this to renderProps fn so we can access
// this while creating our custom methods/properties
renderProps = renderProps.bind(this)
propsToPass = () => this.renderProps(this.stateHandler(), this.props)
render() {
return renderPropsUtil(this.props, {
...this.stateHandler(),
...this.propsToPass(),
})
}
}
}
I'm still working locally on this idea, but before proceeding I'd like to hear from you.