azrahel
azrahel

Reputation: 1213

React Higher Order Elements - how far can one go with passing props

am refactorizing huge project in which

I've thought and searched for ways to handle this issue here and there and found this great article about Higher Order Components (HOC) - basically being components that wrap another components.

https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.8vr464t20

I will now give you (A) example of two out of eight similar components types that I need to handle, than (B) I will paste the code I came up with that unifies those eight files into one. Finally (C) will paste example of usage of that unified component.

I will try to be consistent and naming will be not domain driven (I can't post project details here) but same names in different code excerpts below will always point to same components and data. Otherwise I will point it.

(A) - similar components types

1. TabA - Simple one

export default class TabA extends Component {
  render() {
    return (
    <PageWrapper>
      <Grid>
        <GridItem xsSize="3">
          <SmartComponent something={this.props.something }/>
        </GridItem>
        <GridItem xsSize="9">
          <Tabs 
            permalink = { this.props.permalink } 
            history={ this.props.history } 
            activeTab={ Paths.somePath }
          />
          <TabAContent
            data={ this.props.data }
            name={ this.props.name }
            someValue={ this.props.someValue } 
          />
        </GridItem>
      </Grid>
    </PageWrapper>
  );
}

}

Notice that SomeComponentA does not take any children in. Also there is no conditional rendering of any kind here.

2. TabB - More complex one

Similarly here, notice that renderSomeData method conditionally renders SmartComponentToBeConditionallyRendered and also SomeComponentB takes children in from the props.

export default class TabB extends Component {
  renderSomeData() {

    let someData = {
      header: "Header text",
      searchPlaceHolder: 'Search (name)',
      buttonCaption: 'button caption'
    };

    return (
    <SmartComponentToBeConditionallyRendered
      type={ 'some_type' }
      permalink={ this.props.permalink }
      data={ someData }
    />
  )
}

render() {
  let { data } = this.props;

  return (
    <div>
      <PageWrapper>
        <Grid>
          <GridItem xsSize="3">
            <SmartComponent something={this.props.something}/>
          </GridItem>
        <GridItem xsSize="9">
          <Tabs 
            permalink = { this.props.permalink } 
            history = { this.props.history } 
            activeTab = { Paths.somePage }
          />
          <TabBContent data = { data }>
            {this.props.children}
          </TabBContent>
        </GridItem>
      </Grid>
    </PageWrapper>
    {
      this.context.hasPermission('somePermission') ?
        this.renderSomeData() :
        null
    }
    </div>
  )
}
static contextTypes = {
  hasPermission: React.PropTypes.func.isRequired
}

}

Those eight components I've wrote about at the beginning - they all represent one of three possibilities. Two pictured above and possibility C, but differences in C are just another conditionally rendered components so not worth mentioning cause it will finally come down to passing more flags in props.

Those two components above - they differ in:

(B) What I came up with

let availablePartials = {
  PartialA: PartialA,
  PartialB: PartialB,
  PartialC: PartialC
}

export default class GenericTab extends Component {
  renderSomeData() {
    return (
      <SomeData
        type      = { this.props.type }
        permalink = { this.props.permalink }
        data      = { this.props.someData } //PASSED FROM PROPS NOW
      />
    );
  }

  render() {
    let  tabContent = React.createElement(
      availablePartials[this.props.partialView.name], 
      this.props.partialView.props, 
      this.props.renderChildren ? this.props.children : null
    );

    return (
      <div>
        <PageWrapper>
          <Grid>
            <GridItem xsSize="3">
              <SmartComponent something = { this.props.permalink }/>
            </GridItem>
            <GridItem xsSize = "9">
              <Tabs 
                permalink = { this.props.permalink } 
                history   = { this.props.history }
                activeTab = { this.props.activeTab }
              />
               { tabContent }
            </GridItem>
          </Grid>
        </PageWrapper>
        {
          this.context.hasPermission(this.props.requiredPermission) &&     this.props.dataForSomeDataMethod ?
            this.renderSomeData() 
            : null
        }
      </div>
    )
  }
  static contextTypes = {
    hasPermission: React.PropTypes.func.isRequired
  }
};

CityPageTab.propTypes = {
  permalink: PropTypes.string,
  dataForSomeDataMethod: PropTypes.object,
  type: PropTypes.string,
  activeTab: PropTypes.string,
  renderChildren: PropTypes.bool,
  partialView: PropTypes.object,
  requiredPermission: PropTypes.string
};

Basically EVERYTHING is constructed from props. The only part I don't like is availablePartials[this.props.partialView.name]. It requires developers to keep the state of availablePartials object consistent and tangles it a bit. Not nice solution but still it is best I came up with so far.

(C) New GenericTab usage example

componentThatUseGenericTabRenderMethod() {
  let { valueA, valueB, valueC, history } = this.props;
  let someData = {
    header: 'header text',
    searchPlaceHolder: 'Search (name)',
    buttonCaption: 'buttonCaption'
  }

  return (
    <GenericTab
      partialView = {{
        name: 'PartialA', 
        props: {
          A: valueA,
          B: valueB,
          C: valueC,
          history: history,
          permalink: this.props.params.permalink
        }
      }}
      permalink = { this.props.params.permalink } 
      activeTab = { Paths.somePath }
      someData = { someData }
      type = { 'SOME_TYPE' }
      renderChildren = { false }
      requiredPermission = { 'some_required_permision' }
    />
  );
}

So that is that. Usage got bit more complex, but I got rid of seven files (and getting rid of files is main objective as there is too many of them) and am going to further push it in similar manner - generic one. Thing with genericity - it is more difficult to use but saves lots of space.

Project utilises Redux so dont be too concerned about passing props down the tree. They always only come from some SmartParentComponent that renders GenericTab

Below is the visualisation of how it looks on the page. GenericTab is responsible for rendering Tabs and TabContent parts. Yes I know it is shitty solution, but am not responsible for architecture of it. There are so many things to be refactorized here and what am asking about is just a step in a journey. So please lets focus on the question asked and not other things that are so wrong with this code. I know.:)

VISUALISATION

Guess I could make an article out of it but I don't really have blog to do it:).

Please tell me what you think, propose upgrades, different ways of handling this problem etc.

Upvotes: 2

Views: 249

Answers (1)

Pierre Criulanscy
Pierre Criulanscy

Reputation: 8686

When dealing with such architecture (i.e, tabs in your case), you basically don't want to hide the architecture under the hood, because in this case your ending up adding more and more properties with each new case you want to handle.

Instead, you wan't to let react handles the nested structure since it's where react really shines. That let you write something very generic by handling the built in children props. You typically want to write something like :

const PageWithTabs = (props) => (
  <Tabs defaultActive={'targaryens-panel'}>
    <TabBar>
      <TabLink href="#starks-panel">{'Starks'}</TabLink>
      <TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
      <TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
    </TabBar>
    <TabPanel id="starks-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Eddard</li>
        <li>Catelyn</li>
        <li>Robb</li>
        <li>Sansa</li>
        <li>Brandon</li>
        <li>Arya</li>
        <li>Rickon</li>
      </ul>
    </TabPanel>
    <TabPanel id="lannisters-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Tywin</li>
        <li>Cersei</li>
        <li>Jamie</li>
        <li>Tyrion</li>
      </ul>
    </TabPanel>
    <TabPanel id="targaryens-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Viserys</li>
        <li>Daenerys</li>
      </ul>
    </TabPanel>
  </Tabs>
)

The point here is that you don't have to "predict" all the things that might appear under each TabPanel, simply let the developper put whatever he wants ! BUT me need some logic to handle the "go to tab" sort of things.

React provides some very handy utilities methods to dynamically clone elements, map over elements, and render element whether its type is the one you expect or not (in our case, we expect TabBar or TabPanel type, nothing prevent the developper to put any other components than this two but nothing prevent him neither to put any built in <table> html element inside of <a> tag or something weird like that).

Here is a little implementation with Material Design Lite, it's not perfect but you should get the point :

class Tabs extends React.Component {
  constructor(props) {
    super(props),
    this.state = {
      activeTabId: props.defaultActive
    }
  }
  tabClickHandlerFactory(id) {
    return (e) => {
      e.preventDefault()
      this.setState({
        activeTabId: id
      })
    }
  }
  getPanelIdFromLink(href) {
      return href.split('#')[1]
  }
  render() {
    const self = this

    return (
      <div className='mdl-tabs is-upgraded' {...self.props}>
        {React.Children.map(self.props.children, (child, index) => {
          if (child.type == TabBar) {
            return React.cloneElement(child, {}, React.Children.map(child.props.children, (link) => {
              const id = self.getPanelIdFromLink(link.props.href)
              return (
                React.cloneElement(link, {
                    onClick: self.tabClickHandlerFactory(id),
                    active: self.state.activeTabId === id
                })
              )
            }))
          }
          if (child.type == TabPanel) {
            const { id } = child.props
            const active = self.state.activeTabId === id
            return active && React.cloneElement(child, { active: true })
          }
        })}
      </div>
    )
  }
}

Tabs.propTypes = {
  defaultActive: React.PropTypes.string.isRequired,
}

const TabBar = (props) => <div className='mdl-tabs__tab-bar' {...props}>{props.children}</div>

const TabLink = ({ active, ...props }) => {
  return (
    <a className={`mdl-tabs__tab${active ? ' is-active' : ''}`} {...props}>{props.children}</a>
  )
}


const TabPanel = ({ active, ...props }) => (
  <div className={`mdl-tabs__panel${active ? ' is-active' : ''}`} {...props}>{props.children}</div>
)

const PageWithTabs = (props) => (
  <Tabs defaultActive={'targaryens-panel'}>
    <TabBar>
      <TabLink href="#starks-panel">{'Starks'}</TabLink>
      <TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
      <TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
    </TabBar>
    <TabPanel id="starks-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Eddard</li>
        <li>Catelyn</li>
        <li>Robb</li>
        <li>Sansa</li>
        <li>Brandon</li>
        <li>Arya</li>
        <li>Rickon</li>
      </ul>
    </TabPanel>
    <TabPanel id="lannisters-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Tywin</li>
        <li>Cersei</li>
        <li>Jamie</li>
        <li>Tyrion</li>
      </ul>
    </TabPanel>
    <TabPanel id="targaryens-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Viserys</li>
        <li>Daenerys</li>
      </ul>
    </TabPanel>
  </Tabs>
)

ReactDOM.render(<PageWithTabs/>, document.getElementById('app'))
<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>
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.brown-orange.min.css" /> 
<div id='app'></div>

Upvotes: 2

Related Questions