- Published on
Building <Tabs /> and <Accordion /> components along with React Render Props.
- Authors
- Name
- Aldren Terante
- @aldrenterante
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
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,
};
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.
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!