Published on

Building <Tabs /> and <Accordion /> components along with React Render Props.

Authors

Most of the times both of these components have the same expectations and logic for the state and can accept the same array structure of data like this:

const items = [
  {
    title: 'first title',
    content: 'your first content',
  },
  {
    title: 'second title',
    content: 'your second content',
  },
  {
    title: 'third title',
    content: 'your third content',
  },
  {
    title: 'fourth title',
    content: 'your fourth content',
  },
];

And we can add the items to the following components like this:

<Tabs items={items} />
<Accordion items={items} />

Yes, there are period were we need to accomplish a goal in order meet the details so we ca push it to production. There are some cases (like for this alike components) that we do a quick copy paste solution in order to meet the expectations.

Yehey! we just push it to production and we settle a feature were it affects the revenue for this week after we implement those componets! Business is business and were glad that the business owners are happy. But the feeling of despair and your OCD symptoms starts to kick in. That you clearly see and violate yourself because to can still make it more better

Problem starts to kick in

So lets try to write it down with a function component and for class naming lets use BEM convention. I also add a simple css styling and active state for both components.

Tabs.js tabs

const Tabs = (props) => (
  <div className="Tabs">
    <div className="Tabs__menu">
      {props.items.map(({ title }, index) => {
        const setActiveClassName = index === 0 ? ' Tabs__menu__item--active' : '';
        return (
          <div
            key={index}
            className={`Tabs__menu__item${setActiveClassName}`}
          >
            {title}
          </div>
        );
      })}
    </div>
    <div className="Tabs__body">
      {props.items.map(({ content }, index) => {
        const setActiveClassName = index === 0 ? ' Tabs__content--active' : '';
        return (
          <div
            key={index}
            className={`Tabs__content${setActiveClassName}`}
          >
            {content}
          </div>
        );
      })}
    </div>
  </div>
);

Tabs.defaultProps = {
  activeIndex: 0,
};
accordion
const Accordion = (props) => (
  <div className="Accordion">
    {props.items.map(({ title, content }, index) => {
      const setActiveClassName = index === props.activeIndex ? ' Accordion__item--active' : '';
      return (
        <div
          key={index}
          className={`Accordion__item${setActiveClassName}`}
        >
          <div className="Accordion__menu">
            {title}
          </div>
          <div className="Accordion__content">
            {content}
          </div>
        </div>
      );
    })}
  </div>
);

Accordion.defaultProps = {
  activeIndex: 0,
};

Both components have different layout structure but have the same requirements for the props items. For this part we clearly see the separation of concerns for html structure and styling. Also the way we set the first item to be active is by checking if the current key is match for the default set value for activeIndex whick is equal to 0 since most of the time the first element for both component are expected to be active or open.

But is it possible to share the same props key and value without any headache? Yes indeed!

Sharing it with Render Props!

Base on the documentation:

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

https://reactjs.org/docs/render-props.html

Lets create a Tabcordion.js with default props of activeIndex and write this like:

class Tabcordion extends Component {
  constructor(props) {
    super(props);
    this.state = {
      activeIndex: props.activeIndex,
    };
  }

  setActiveIndex = (index) => {
    this.setState({
      activeIndex: index,
    });
  }

  render() {
    const { activeIndex } = this.state;
    return this.props.children({
      activeIndex,
    });
  }
}

Tabcordion.defaultProps = {
  activeIndex: 0,
}

Tabcordion have the same prop value for activeIndex and we also set it up to a state in the constructor. The main difference with the render props implementation is what we return after. Most of the time we return a component but this time we can return the state or function and can be reuse by any component.

To use the render props we simple do the normal open and close for creating a component but inside we need the function with the declared state earlier that we pass it as a props when using it.

const Accordion = (props) => (
  <Tabcordion
    items={props.items}
    activeIndex={props.activeIndex}
  >
    {({ activeIndex, setActiveIndex }) => {
      return (
        <div className="Accordion">
          {props.items.map(({ title, content }, index) => {
            const setActiveClassName = index === activeIndex ? ' Accordion__item--active' : '';
            return (
              <div
                key={index}
                className={`Accordion__item${setActiveClassName}`}
                onClick={() => setActiveIndex(index)}
              >
                <div className="Accordion__menu">
                  {title}
                </div>
                <div className="Accordion__content">
                  {content}
                </div>
              </div>
            );
          })}
        </div>
      );
    }}
  </Tabcordion>
);

const Tabs = (props) => (
  <Tabcordion
    items={props.items}
    activeIndex={props.activeIndex}
  >
    {({ activeIndex, setActiveIndex }) => {
      return (
        <div className="Tabs">
          <div className="Tabs__menu">
            {props.items.map(({ title }, index) => {
              const setActiveClassName = index === activeIndex ? ' Tabs__menu__item--active' : '';
              return (
                <div
                  key={index}
                  className={`Tabs__menu__item${setActiveClassName}`}
                  onClick={() => setActiveIndex(index)}
                >
                  {title}
                </div>
              );
            })}
          </div>
          <div className="Tabs__body">
            {props.items.map(({ content }, index) => {
              const setActiveClassName = index === activeIndex ? ' Tabs__content--active' : '';
              return (
                <div
                  key={index}
                  className={`Tabs__content${setActiveClassName}`}
                >
                  {content}
                </div>
              );
            })}
          </div>
        </div>
      );
    }}
  </Tabcordion>
);

But how can we update the activeIndex to the new one? As you can see we also add up the setActiveIndex function requiring the key value of the selected item onClick. The activeIndex will be updated once the user tries to select for both component.

For more information you can access the sample Tabcordion in this repository https://github.com/thecodingwhale/tabcordion to see the full working components.

Thank you!