I'm currently reviewing some different ways to have reusable components enforcing both most of the style & logic. The use case is a generic MainList
with children components (eg. MainListToolbar
, MainListContent
, etc.) that can all be used together to create multiple mostly agnostic lists for business objects (eg. UserList
, DeviceList
, etc.).
For instance, I want to abstract away a multi-selectable
logic and design in MainListContent
while still being able to access the related internal state in the parent UserList
(eg. this.state.selectedItems
). Ideally, the multi-selectable
logic (eg. onListItemClick
, onListItemDoubleClick
, etc.) should not leak at all into UserList
.
I first thought about extending some MainListComponent
(instead of Component
), but I've never seen it in the React world and looks like a bad pattern to me.
I got something working with a state delegation pattern that enables to have an UserList
that implements generic children elements and inherits the abstracted logic (eg. listItem selection) in it's own state to use it any way it wants.
Is there something flawed in this pattern/code? Is there a more elegant/safe way to achieve that?
const {Component} = React;
class MainList extends Component {
render() {
const {children} = this.props;
return (
<div>{children}</div>
);
}
}
class MainListContent extends Component {
constructor(props) {
super(props);
this.state = {
selectedItems: []
};
this.handleListItemClick = (ev) => {
const {selectedItems} = this.state;
const value = ev.target.innerHTML;
this.setState({
selectedItems: !selectedItems.includes(value) ? selectedItems.concat(value) : selectedItems.filter(item => item !== value)
});
}
}
setState(nextState) {
super.setState(nextState);
const {onSetState} = this.props;
if (onSetState) {
onSetState(nextState);
}
}
render() {
const {title, items} = this.props;
const {selectedItems} = this.state;
return (
<div>
<h5>{title}</h5>
<ul>
{items.map((item, index) => (<li key={index} onClick={this.handleListItemClick} style={{cursor: 'pointer', fontWeight: selectedItems.includes(item) ? 'bold' : 'normal'}}>{item}</li>))}
</ul>
</div>
);
}
}
class UserList extends Component {
constructor(props) {
super(props);
this.state = {};
this.delegateSetState = this.setState.bind(this);
}
render() {
const {items} = this.props;
const {selectedItems} = this.state;
return (
<MainList>
<MainListContent ref={ref => { this.mainListContent = ref; }} onSetState={this.delegateSetState} title="Users" items={items} />
<pre>count: {selectedItems ? selectedItems.length : 0}</pre>
</MainList>
)
}
}
const Root = props => (
<UserList items={['foo', 'bar', 'baz']}></UserList>
);
ReactDOM.render(
<Root />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>