2
\$\begingroup\$

I've written a simple tabbed component in ReactJS, whose purpose was to make defining different tabs very simple. My implementation does this very well in my opinion, since defining tabs becomes very simple:

<TabbedComponent>
  <Tab>
    <Name>Tab 1</Name>
    <Content>
      <h1>This is the first tab</h1>
      <p>This is the first tab content</p>
    </Content>
  </Tab>
  <Tab>
    <Name>Tab 2</Name>
    <Content>
      <h1>This is the second tab</h1>
      <p>This is the second tab content</p>
    </Content>
  </Tab>
</TabbedComponent>

My solution works fine, but the code seems kind of hacky:

import './TabbedComponent.scss';

import React from 'react';
import InlineComponent from './InlineComponent.react';

/**
 * Tabbed component, using an inline layout.
 *
 * Usage:
 * <TabbedComponent>
 *
 *   // If you only have one tab, what's the point ^^
 *   <Tab>
 *
 *     Names must be unique
 *     <Name>The display name of the tab in the sidebar</Name>
 *
 *     You can stick anything you want in here
 *     <Content>This will be rendered inside the viewbox</Content>
 *
 *   </Tab>
 * </TabbedComponent>
 */
class TabbedComponent extends React.Component {

  displayName = 'TabbedComponent';

  state = {
    activeChild: null
  };

  constructor(props) {
    super(props);

    this._handleTabClick = this._handleTabClick.bind(this);
  }

  componentWillMount() {
    let tabs = React.Children.toArray(this.props.children)
        .filter(child => child.type === Tab);

    // default to first tab as active
    if (this.state.activeChild === null) {
      let first = tabs[0].props.children.filter(
          child => child.type === Name)[0];
      this.setState({ activeChild: first.props.children })
    }
  }

  render() {
    // extract only Tab children, anything else is invalid and will be ignored
    let tabs = React.Children.toArray(this.props.children)
        .filter(child => child.type === Tab);

    // extract tab names from own children within Tab elements
    let tabNames = tabs.map(tab => {
      return tab.props.children.filter(child => child.type === Name);
    });
    let tabNamesArray = [].concat.apply([], tabNames);
    let tabNamesElements = tabNamesArray.map(tabName => {
      let tabText = React.Children.only(tabName).props.children;
      return (
          <Name
              key={tabText}
              name={tabText}
              active={tabText == this.state.activeChild}
              onTabClick={this._handleTabClick}>
            {tabText}
          </Name>
      );
    });

    // find the tab content that should be active given a state
    let tabContent = tabs.filter(tab =>
        tab.props.children[0].props.children == this.state.activeChild
    );
    // pass children from tab content down into child
    // traversal: tabContent(Tab) -> 1st child(Content) -> children: text
    tabContent = tabContent[0].props.children[1].props.children;

    return (
        <InlineComponent className="tabbed-component">
          <nav className="col-25 tabbed-nav">
            {tabNamesElements}
          </nav>
          <div className="col-75 tabbed-viewbox">
            <Content>
              {tabContent}
            </Content>
          </div>
        </InlineComponent>
    );
  }

  _handleTabClick(target) {
    if (this.state.activeChild != target) {
      this.setState({ activeChild: target });
    }
  }
}

/**
 * Tab is more or less of a semantical element, no render value, but it makes
 * defining tabs very easy.
 */
class Tab extends React.Component {

  displayName = 'TabbedComponentTab';

  constructor(props) {
    super(props);
  }

  render() {
    return null;
  }
}

/**
 * This is the component that displays in the tab navigation. When the element
 * is active, the `active` class is added.
 */
class Name extends React.Component {

  displayName = 'TabbedComponentName';

  constructor(props) {
    super(props);

    this._handleClick = this._handleClick.bind(this);
  }

  render() {
    return (
        <span
            className={'tabbed-nav-tab' + (this.props.active ? ' active' : '')}
            onClick={this._handleClick}>
          {this.props.children}
        </span>
    );
  }

  _handleClick(e) {
    e.preventDefault();
    // Tell the top level tabbed component that it should switch tabs
    this.props.onTabClick(this.props.name);
  }
}

/**
 * This is the component that renders in the viewport.
 */
class Content extends React.Component {

  displayName = 'TabbedComponentContent';

  constructor(props) {
    super(props);
  }

  render() {
    return (
        <div className="tabbed-content">
          {this.props.children}
        </div>
    );
  }
}

export default TabbedComponent;
export { Tab, Name, Content };

I don't really know how to explain it, but the code doesn't seem particularly clean to me; there's a lot of direct array access e.g. tabContent[0].props.children[1].props.children;, but I can't really see how I would get around that.

Any feedback would be appreciated.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

My suggestion is that instead of your TabbedComponent receiving children which are Tab components, it should receive an array of objects that can be used to build Tab components, and the Name and Content classes should become implementation details that the user of the TabbedComponent doesn't need to know about (or maybe even diappear entirely). The interface would be significantly different, but also more difficult to use incorrectly, and I think it would make the implementation cleaner:

<TabbedComponent
  tabs={[
    {
      name: "Tab 1",
      //contents could also be an array of elements
      contents: <div>
        <h1>This is the first tab</h1>
        <p>This is the first tab content</p>
      </div>,
    },
    {
      name: "Tab 2",
      contents: <div>
        <h1>This is the second tab</h1>
        <p>This is the second tab content</p>
      </div>
    },
  ]}
/>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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