Last active
January 9, 2018 17:17
-
-
Save mturley/b75732a1b740009928ca31ab38005c35 to your computer and use it in GitHub Desktop.
A Higher-Order React Component for Controlled State
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
const filterKeys = (obj, callback) => | |
Object.keys(obj) | |
.filter(callback) | |
.reduce((values, key) => ({ ...values, [key]: obj[key] }), {}); | |
const nullValues = obj => | |
Object.keys(obj).reduce((values, key) => ({ ...values, [key]: null }), {}); | |
/* | |
controlled(stateTypes, defaults)(WrappedComponent) | |
This Higher Order Component provides the controlled component pattern on a prop-by-prop basis. | |
It's a nice way for components to implement internal state so they "just work" out of the box, | |
but also give users the option of lifting some or all of that state up into their application. | |
controlled() takes two arguments: | |
* stateTypes - an object of PropTypes for the state that will be contained here | |
* defaults - an optional object with default values for stateTypes | |
The WrappedComponent will be rendered with special props: | |
* setControlledState - a reference to this state wrapper's this.setState. | |
* Props for all the stateTypes, from this.props if present or from this.state otherwise. | |
* All other props passed to the controlled component HoC. | |
The idea is that the values in stateTypes could be stored in state, or passed in via props. | |
The WrappedComponent doesn't have to care which is being used, and can manage the state | |
contained here. When present, props are used instead. If you provide these special props, | |
be sure to also provide corresponding callbacks/handlers to keep them updated. | |
*/ | |
export const controlled = (stateTypes, defaults = {}) => WrappedComponent => { | |
class ControlledComponent extends React.Component { | |
constructor() { | |
super(); | |
this.state = { ...nullValues(stateTypes), ...defaults }; | |
this.setControlledState = this.setControlledState.bind(this); | |
} | |
setControlledState(updater) { | |
this.setState(updater); | |
} | |
render() { | |
const controlledStateProps = filterKeys( | |
this.props, | |
key => stateTypes.hasOwnProperty(key) && this.props[key] !== null | |
); | |
const otherProps = filterKeys( | |
this.props, | |
key => !stateTypes.hasOwnProperty(key) | |
); | |
return ( | |
<WrappedComponent | |
setControlledState={this.setControlledState} | |
{...this.state} | |
{...controlledStateProps} | |
{...otherProps} | |
/> | |
); | |
} | |
} | |
ControlledComponent.displayName = WrappedComponent.displayName; | |
ControlledComponent.propTypes = { | |
...WrappedComponent.propTypes, | |
...stateTypes | |
}; | |
ControlledComponent.defaultProps = WrappedComponent.defaultProps; | |
return ControlledComponent; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import PropTypes from 'prop-types'; | |
class MyComponent extends React.Component { | |
someMethod() { | |
const { setControlledState } = this.props; | |
setControlledState({ value: 'New Value' }); | |
} | |
render() { | |
const { value } = this.props; | |
return <h2>Value: {value}</h2>; | |
} | |
} | |
const controlledStateTypes = { | |
value: PropTypes.string | |
}; | |
MyComponent.propTypes = { | |
...controlledStateTypes, | |
moreProps: PropTypes.string, | |
andStuff: PropTypes.string, | |
}; | |
const defaultControlledState = { | |
value: 'Default Value' | |
}; | |
MyComponent.defaultProps = { | |
moreProps: 'etc', | |
andStuff: 'etc' | |
}; | |
export default controlled(controlledStateTypes, defaultControlledState)(MyComponent); | |
// You can use <MyComponent value="Some Value" /> and value will always be "Some Value" whether someMethod is called or not. | |
// Or you can use <MyComponent /> and the value will start as "Default Value" and become "New Value" when someMethod is called. |
I think I will refactor controlled()
internally, but yeah I'll leave it and the Vert Nav components that are based on it can stay the same. I'll finish the nav components before I fix up this HoC. Once it's fixed up I could still submit it as a PR to recompose if it's still useful as a shortcut to using withState in a particular way.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this looks like a very useful extension! Based on the response in your upstream issue, I am guessing we can do something to the effect of
return withState(ControlledComponent)
on line 66 in controlled.js... I still think generalizing this pattern as HOCcontrolled()
is nice though.