Creating a React Tabs component with custom buttons.

This post is the Lesson 4 of React online course from JS Mega Tools. You can get the code for the previous lesson at the following address: https://github.com/jsmegatools/React-online-course  Once you’ve cloned the repository you can go into Lesson-3 folder and edit files the way its done in this tutorial.

In the previous lesson we set up Redux for our application and now have a state management solution, container components and presentational components. Now it is time to work on our presentational components.

The main area of our application is going to feature several categories by which we are going to perform accommodations search: locations (Which we have already added and which are displayed when you request a home page url), amenities and a type of an accommodation. 

While it may be ok to put everything on one page and have a user scroll down to select items from every category, it is better for a user to be able to bring up items from a category he/she wants by clicking on buttons, that are located next to each other. This is the functionality that is achievable by splitting content into sections and creating tabs to open these sections. So our task right now is to create a tabs component for React.

Tabs occur often in UIs and you may have several parts of your application using tabs, or maybe you work on several projects, which all make use of UI tabs. It would be beneficial to create a reusable tabs component and that is what we are going to do.

First, we are going to add some edits to react-ui/src/features/MainArea/MainArea.js. Add the following import:

import { Tabs, TabSection } from '../Tabs/Tabs';

We are going to create the imported components later. Then change the render method to look like the following:

render() {
    const locations = this.props.isFetching ? <RefreshIndicator
      size={50}
      top={0}
      left={0}
      loadingColor="#FF9800"
      status="loading"
      style={{
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%,-50%)'
      }}
    /> :
      this.props.locations.map(location =>
        <li key={location.id}>
          <img src={location.image} alt={location.name} />
          {location.name}
        </li>)
    return (
      <div className="home-page-container">
        <Tabs>
          <TabSection name="Locations">{locations}</TabSection>
          <TabSection name="Amenities">A list of amenities</TabSection>
          <TabSection name="Type">A list of types</TabSection>
        </Tabs>
      </div>
    );
  }

As you can see, Tabs is a component that wraps TabSection components. Tabs is the main component, that holds the internal state and coordinates actions between tab buttons and tab content.

TabSections are used to set content of tabs and names of tab buttons. Moreover we can use a render prop of TabSection to set the structure of a tab button, so that it does not only has text in it but any markup you want. For example you can add an icon next to the text, or some other complex markup. The way to use a render prop with TabSection is like the following:

<TabSection render={() => <CustomTabButton />}>A list of types</TabSection>

It’s important to remember to avoid using arrow functions like the above as values of props for performance reasons. It’s better to use a bound method of the component like this:

<TabSection render={this.createCustomTabButton}>A list of types</TabSection>

Where createCustomTabButton is a bound method.

For locations tab we are setting the content of TabSection to locations constant from above.

Next, go into react-ui/src/features folder and create a Tabs folder that will hold a Tabs component. Then go into that folder and create a Tabs.js file with the following content:

import React, { Component } from 'react';
import TabButton from './TabButton/TabButton';
import TabSection from './TabSection/TabSection';
import './styles.css';

class Tabs extends Component {
  constructor() {
    super();
    this.state = {
      activeTab: 0
    };
    this.setActiveTab = this.setActiveTab.bind(this);
  }

  setActiveTab(activeTab) {
    this.setState({ activeTab });
  }

  renderTabs() {
    return React.Children.map(
      this.props.children,
      (child, index) =>
        <TabButton
          {...child.props}
          setActiveTab={this.setActiveTab}
          active={this.state.activeTab === index}
          index={index}
        />
    )
  }

  renderActiveSection() {
    return React.Children.toArray(this.props.children)
      .filter((child, index) => {
        return index === this.state.activeTab;
      });
  }

  render() {
    return (
      <div>
        <ul className="jsmt-tab-buttons">
          {this.renderTabs()}
        </ul>
        {this.renderActiveSection()}
      </div>
    );
  }
}

export { Tabs, TabSection };

This is the main file of the Tabs component, if you were to create an npm package, the filename would be the value of main property in that package’s package.json file. Let’s look at what is going on here. We are importing TabButton and TabSection components. We are going to create TabButton component later, and reexport TabSection from this module. Reexporting helps to make imports more convenient, so that a user can import all components from one module.

Then we have a class constructor which calls super method with props (you always have to call the super method when you are using a constructor in React). This is a component we actually want to use local state in (that is a state managed by React and not by Redux).

The reason for thats that is this component is self contained. That is it does not use any data from other parts of the application and it does not send any data or modify any data in other parts of the application. The other reason is that we are creating this component as a reusable component and we want it to interact with as little of external things as possible.

We are initializing the state with activeTab property which is a number which represents the tab that is currently active. Then we are binding setActiveTab method of the component to the current instance. This is a common pattern when you write React components ES6 style (with classes rather than React.createElement, ES6 style is used the most nowadays in React development, so we are going to stick to it). The purpose of the binding is for `this` inside a bound method to refer to the component instance, when otherwise it would not.

setActiveTab method gets an active tab’s index as an argument and uses setState method to save the index to the state as activeTab property.

The next method of Tabs component is renderTabs. It uses React.Children.map to iterate over the children of Tabs component (remember in MainArea we put several TabSection in Tabs as children). React.Children has several methods to work with a component’s children. You can access children of a component as this.props.children. For each TabSection passed we create a TabButton component which is passed props of TabSection (such as render or name), a setActiveTab method, active prop which indicates whether this button is currently active, and an index prop, which is the number of this TabButton.

The next method is renderActiveSection, which returns an active section by first converting the component’s children to array, then using filter method of the array. In filter it compares the index of the section with an active tab’s index from the component state.

In render method we create some additional markup and then call the above methods.

Finally we export the essential components.

We are using some styles here also, let’s create a styles.css file with the following contents:

.jsmt-tab-buttons {
  display: flex;
  list-style-type: none;
}

We also need to set up a TabButton component. Let’s create a TabButton folder, then in that folder create TabButton.js file with the following contents:

import React, { Component } from 'react';
import './styles.css'

class TabButton extends Component {
  constructor() {
    super();
    this.setActiveTab = this.setActiveTab.bind(this);
  }

  setActiveTab() {
    this.props.setActiveTab(this.props.index);
  }

  renderContent() {
    const { render, name, index } = this.props;
    if (render) {
      return render();
    } else if (name) {
      return name;
    }
    return index;
  }

  render() {
    return (
      <li
        className="jsmt-tab-button"
        onClick={this.setActiveTab}
      >
        {this.renderContent()}
      </li>
    );
  }
}

export default TabButton;

The component is going to have its own setActiveTab method, which is basically needed to call setActiveTab method of its parent with this.props.index (a numeric position of the current tab among other tabs).

Then we have renderContent method which checks what props the instance has received and renders a button’s content accordingly. Here is how render prop works from inside the component that receives it. You can see that using a render prop is as simple as calling a function. You can call this function with whatever arguments you want, as long as that function makes use of them. Finally as a default case we return the number of the tab.

In render method we create a button as a li element, whose onClick handler calls TabButton’s setActiveTab method.

TabButton has some styles of its own, lets create a styles.css file for it with the following contents:

.jsmt-tab-button {
  padding: 20px;
  cursor: pointer;
}

The only component that is needed for tabs functionality is TabSection. Let’t create a TabSection folder in Tabs folder, then in TabSection folder create TabSection.js file with the following contents:

import React, { Component } from 'react';

class TabSection extends Component {
  render() {
    return (
      <div>{this.props.children}</div>
    );
  }
}

export default TabSection;

As you can see this component is as simple as wrapping its children in whatever element you decide appropriate.

Tabs component is now ready to be used.

Source code for this and the preceding lessons can be found at the following address:

https://github.com/jsmegatools/React-online-course

Leave a Reply

Your email address will not be published. Required fields are marked *