1
\$\begingroup\$

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>

\$\endgroup\$

migrated from stackoverflow.com Sep 4 '16 at 13:05

This question came from our site for professional and enthusiast programmers.

1
\$\begingroup\$

One prevailing pattern is stateless components. You will most likely have components that require state, but it is easier to follow the flow of data (and write tests) if your components rely solely on props. The stateless pattern is somewhat similar to yours (in that they are both mutating state in the parent), but instead of proxying calls to setState on the top level component AND having to re-implement setState in each component, child components just call the methods passed from the top level to mutate the state and then receive the new data in the form of props.

From React Docs:

In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible.

Here is your example using stateless components (except for the top level component):

const { Component } = React;

const MainList = (props) => {
   const {children} = props;
   return (
     <div>{children}</div>
   );
}

const MainListContent = (props) => {
   const {title, items} = props;
   return (
      <div>
      	<h5>{title}</h5>
        <ul>{
            items.map((item, index) => (
               <li key={index} 
                   onClick={(e) => props.onSelectItem(item.name)} 
                   style={{cursor: 'pointer', fontWeight: item.selected ? 'bold' : 'normal'}}
               >{item.name}</li>
              ))
        }</ul>
      </div>
   );
}

class UserList extends Component {
  constructor(props) {
    super(props);
    this.state = {
        items: props.items.map(item => ({
            selected: false,
            name: item
        }))
    }
  }
  onSelectItem(itemName) {
    let { items } = this.state;
    this.setState({
        items: items.map(item => {
            if (item.name === itemName) {
                item.selected = !item.selected;
            }
            return item;
        })
    });
  }
  render() {
    const { items } = this.state;
  	return (
      <MainList>
        <MainListContent ref={ref => { this.mainListContent = ref; }} onSelectItem={this.onSelectItem.bind(this)} title="Users" items={items} />
        <pre>count: {items.filter(item => item.selected).length}</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.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="root"></div>

\$\endgroup\$
  • \$\begingroup\$ Thank you for the response! However, the issue here is that onSelectItem logic leaks into UserList, that's precisely what I was trying to avoid. In my actual business code, I have dozen of MainList-related-functions that I definitely don't want to reimplement with traditionnal onSomething handlers for each one, and everytime I setup a list. \$\endgroup\$ – Olivier Sep 2 '16 at 23:28
  • \$\begingroup\$ @Olivier have you considered using Redux? It sounds like you want to use "container components". The other option is to use contexts \$\endgroup\$ – Sal Sep 2 '16 at 23:58
1
\$\begingroup\$

I think you can use Inheritance Inversion High Order Components to solve your problem. Try this :

class MainListContent extends Component {
   ...
  /* DELETE THIS FUNCTION
  setState(nextState) {
  super.setState(nextState);
  const {onSetState} = this.props;
   if (onSetState) {
  onSetState(nextState);
  }
  }  */      
   ...

}

const addCounter = (WrappedComponent) => {
  return class Enhancer extends WrappedComponent {
    render(){
      const {selectedItems} = this.state;
      return(
        <MainList>
            {super.render()}
            <pre>count: {selectedItems ? selectedItems.length : 0}</pre>
        </MainList>
      )
    }
  }
}

const UserList = addCounter(MainListContent);

This pattern is good for reading state of WrappedComponent. You should be careful when using the pattern for edit state.

For more info about Higher Order Component, you can read this great article.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.