Skip to content

Instantly share code, notes, and snippets.

@mturley
Last active January 9, 2018 17:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mturley/b75732a1b740009928ca31ab38005c35 to your computer and use it in GitHub Desktop.
Save mturley/b75732a1b740009928ca31ab38005c35 to your computer and use it in GitHub Desktop.
A Higher-Order React Component for Controlled State
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;
};
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.
@priley86
Copy link

priley86 commented Jan 9, 2018

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 HOC controlled() is nice though.

@mturley
Copy link
Author

mturley commented Jan 9, 2018

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