Write React like a pro. React Icon

Follow the ultimate React roadmap.

Creating a tabs component with React

I have to say, this is my first proper component built in React. I tried it out last year and absolutely loved it. Feel free to rip this component apart, suggest best practices and improvements!

Component design

First we’ll want to “design” the markup. Obviously for this I’ll be using the wonderful JSX syntax, so let’s look at what we want to create (this would be used inside the render function so I’ve omitted the rest):

<Tabs>
  <Pane label="Tab 1">
    <div>This is my tab 1 contents!</div>
  </Pane>
  <Pane label="Tab 2">
    <div>This is my tab 2 contents!</div>
  </Pane>
  <Pane label="Tab 3">
    <div>This is my tab 3 contents!</div>
  </Pane>
</Tabs>

This means we need a Tabs component and Pane child component.

Tab Component

This component will do most of the leg work, so let’s start by defining the Class:

const Tabs = React.createClass({
  displayName: 'Tabs',
  render() {
    return (
      <div></div>
    );
  }
});

I’ve added the displayName: 'Tabs' to help with JSX’s debugging (JSX will set this automatically but I’ve added it for clarity for the Component’s name).

Next up I’ve added the render function that returns the chunk of HTML I need.

Now it’s time to show the tab’s contents passed through. I’ll create a “private” method on the Class, it won’t actually be private but its naming convention with the underscore prefix will let me know it is.

const Tabs = React.createClass({
  displayName: 'Tabs',
  _renderContent() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  },
  render() {
    return (
      <div>
        {this._renderContent()}
      </div>
    );
  }
});

I’ve then added the {this._renderContent()} call inside the render function to return my JSX.

At this point, all the tab contents gets pushed into the tab, so it’s not actually working as we’d like it to. Next up is setting up the _renderContent method to take a dynamic child state using an Array index lookup using [this.state.selected].

const Tabs = React.createClass({
  displayName: 'Tabs',
  _renderContent() {
    return (
      <div>
        {this.props.children[this.state.selected]}
      </div>
    );
  },
  render() {
    return (
      <div>
        {this._renderContent()}
      </div>
    );
  }
});

Currently this.state.selected doesn’t exist, so we need to add some default props and states:

const Tabs = React.createClass({
  displayName: 'Tabs',
  getDefaultProps() {
    return {
      selected: 0
    };
  },
  getInitialState() {
    return {
      selected: this.props.selected
    };
  },
  _renderContent() {
    return (
      <div>
        {this.props.children[this.state.selected]}
      </div>
    );
  },
  render() {
    return (
      <div>
        {this._renderContent()}
      </div>
    );
  }
});

I’ve told getDefaultProps to give me the component defaults, and then I’m passing those defaults (or overwritten user options) to bind to the getInitialState returned Object. Using state allows me to mutate the local properties, as props are immutable.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

One thing we want users to do is be able to pass in a default selected tab, this would be passed through an attribute as a Number.

Now the tab content is setup, we need to actually create the clickable tab links and bind the corresponding click events. Let’s add another pseudo “private” method to the component called _renderTitles:

const Tabs = React.createClass({
  ...
  _renderTitles() {
    function labels(child, index) {
      return (
        <li>
          <a href="#">
            {child.props.label}
          </a>
        </li>
      );
    }
    return (
      <ul>
        {this.props.children.map(labels.bind(this))}
      </ul>
    );
  },
  ...
  render() {
    return (
      <div>
        {this._renderTitles()}
        {this._renderContent()}
      </div>
    );
  }
});

This one’s a little more complex, it maps over the this.props.children Nodes and returns the relevant JSX representation of each clickable tab item.

So far each tab item is an <a> element, however no click events are bound. Let’s bind them by adding a handleClick method, which uses preventDefault() to stop the # bouncing when clicked. Then I can update the selected item using this.setState() by assigning the clicked index.

const Tabs = React.createClass({
  ...
  handleClick(index, event) {
    event.preventDefault();
    this.setState({
      selected: index
    });
  },
  ...
});

We can then bind this event listener in the JSX using onClick={this.handleClick.bind(this, index, child)}:

const Tabs = React.createClass({
  ...
  _renderTitles() {
    function labels(child, index) {
      return (
        <li>
          <a href="#">
            {child.props.label}
          </a>
        </li>
      );
    }
    return (
      <ul>
        {this.props.children.map(labels.bind(this))}
      </ul>
    );
  },
  ...
});

Using this.handleClick.bind() allows me to set the context of the handleClick function and pass in the index of the current mapped element.

This now works nicely, but I want to allow the selected tab to be highlighted using an active className:

const Tabs = React.createClass({
  ...
  _renderTitles() {
    function labels(child, index) {
      let activeClass = (this.state.selected === index ? 'active' : '');
      return (
        <li>
          <a href="#">
            {child.props.label}
          </a>
        </li>
      );
    }
    return (
      <ul>
        {this.props.children.map(labels.bind(this))}
      </ul>
    );
  },
  ...
});

This ternary operator allows me to conditionally assign the 'active' String as the className when the this.state.selected value is equal to the index of the currently clicked element. React takes care of the adding/removing classes for all Nodes for me which is fantastic.

Put together we have our completed Tab component:

const Tabs = React.createClass({
  displayName: 'Tabs',
  getDefaultProps() {
    return {
      selected: 0
    };
  },
  getInitialState() {
    return {
      selected: this.props.selected
    };
  },
  handleClick(index, event) {
    event.preventDefault();
    this.setState({
      selected: index
    });
  },
  _renderTitles() {
    function labels(child, index) {
      let activeClass = (this.state.selected === index ? 'active' : '');
      return (
        <li>
          <a href="#">
            {child.props.label}
          </a>
        </li>
      );
    }
    return (
      <ul>
        {this.props.children.map(labels.bind(this))}
      </ul>
    );
  },
  _renderContent() {
    return (
      <div>
        {this.props.children[this.state.selected]}
      </div>
    );
  },
  render() {
    return (
      <div>
        {this._renderTitles()}
        {this._renderContent()}
      </div>
    );
  }
});

Pane Component

The Pane component is much more simple, and simply passes the contents of the component into itself:

const Pane = React.createClass({
  displayName: 'Pane',
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
});

propTypes validation

React is absolutely fantastic with its debugging error messages, and we can improve that inline by using propTypes and the relevant validation of the type. Let’s start with the tab component:

const Tabs = React.createClass({
  ...
  propTypes: {
    selected: React.PropTypes.number,
    children: React.PropTypes.oneOfType([
      React.PropTypes.array,
      React.PropTypes.element
    ]).isRequired
  },
  ...
});

I’ve told React to throw an error if selected is not of type “Number”, and if the Child nodes are not of type “Array” or “Element”.

This means that if somebody passes a property in that gets bound to this.props.selected that isn’t a Number, it’ll throw an error. This allows us to use propery JavaScript Objects in attributes, hooray for that.

// Errors
<Tabs selected="0">
  <Pane label="Tab 1">
    <div>This is my tab 1 contents!</div>
  </Pane>
  <Pane label="Tab 2">
    <div>This is my tab 2 contents!</div>
  </Pane>
  <Pane label="Tab 3">
    <div>This is my tab 3 contents!</div>
  </Pane>
</Tabs>

// Works
<Tabs selected={0}>
  <Pane label="Tab 1">
    <div>This is my tab 1 contents!</div>
  </Pane>
  <Pane label="Tab 2">
    <div>This is my tab 2 contents!</div>
  </Pane>
  <Pane label="Tab 3">
    <div>This is my tab 3 contents!</div>
  </Pane>
</Tabs>

I’m using JSX’s {} syntax to ensure that plain JavaScript runs in between the braces.

Let’s also add some validation to the Pane component:

const Pane = React.createClass({
  ...
  propTypes: {
    label: React.PropTypes.string.isRequired,
    children: React.PropTypes.element.isRequired
  },
  ...
});

I’m telling React here that label is absolutely required and is a String, and that children should be an element and is also required.

Render

Now for the cherry on top, let’s render it to the DOM:

const Tabs = React.createClass({
  displayName: 'Tabs',
  propTypes: {
    selected: React.PropTypes.number,
    children: React.PropTypes.oneOfType([
      React.PropTypes.array,
      React.PropTypes.element
    ]).isRequired
  },
  getDefaultProps() {
    return {
      selected: 0
    };
  },
  getInitialState() {
    return {
      selected: this.props.selected
    };
  },
  handleClick(index, event) {
    event.preventDefault();
    this.setState({
      selected: index
    });
  },
  _renderTitles() {
    function labels(child, index) {
      let activeClass = (this.state.selected === index ? 'active' : '');
      return (
        <li>
          <a href="#">
            {child.props.label}
          </a>
        </li>
      );
    }
    return (
      <ul>
        {this.props.children.map(labels.bind(this))}
      </ul>
    );
  },
  _renderContent() {
    return (
      <div>
        {this.props.children[this.state.selected]}
      </div>
    );
  },
  render() {
    return (
      <div>
        {this._renderTitles()}
        {this._renderContent()}
      </div>
    );
  }
});

const Pane = React.createClass({
  displayName: 'Pane',
  propTypes: {
    label: React.PropTypes.string.isRequired,
    children: React.PropTypes.element.isRequired
  },
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
});

const App = React.createClass({
  render() {
    return (
      <div>


            <div>This is my tab 1 contents!</div>


            <div>This is my tab 2 contents!</div>


            <div>This is my tab 3 contents!</div>


      </div>
    );
  }
});

ReactDOM.render(, document.querySelector('.container'));

And of course the live demo:

By all means this is not a complete solution for someone to use in production, but could be adapted to suit for sure. Please feel free to fork/improve/share :)

P.S big thanks to Ken Wheeler for letting me pester him with syntax and obscure questions.

Learn React the right way.

The most complete guide to learning React ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover