Today I needed to make a tab system.
Perfect for display multiple types of data in a small space, a tab system has two parts :
- The header always display all the tabs labels
- The content part display the data associated to the selected tab
The complexity of this kind of system is that we have a fixed part and a dynamic part, let's see two implementations.
V1 – Simple to code, hard to use
A first idea is to do a simple component with a tabs
prop corresponding to an array of objects with a label
and a content
which can be called like this :
<TabView
tabs={[
{
label : "First tab",
content : <p>My first tab content</p>
},
{
label : "Second tab",
content : <p>My second tab content</p>
},
{
label : "Third tab",
content : <p>My third tab content</p>
}
]}
/>
I could put content into variable, but it's for the example
The corresponding <TabView>
component should look like this :
const TabView = ({tabs}) => {
const [selectedTabIndex, setSelectedTabIndex] = useState(0)
return (
<div>
<div className="header">
{tabs.map(tab => (
<p>{tab.label}</p>
))}
</div>
<div className="content">
{tabs[selectedTabIndex].content}
</div>
</div>
)
}
First problem, I need a conditional tab and with this configuration it's complicated 😕
We have to put the tabs into a variable and add an optional tab if necessary... Something like that :
const displayThirdTab = ...
const tabs = [
{label : "First tab", content : <p>My first tab content</p>},
{label : "Second tab", content : <p>My second tab content</p>}
]
if(displayThirdTab){
tabs.push({label : "Third tab", content : <p>My third tab content</p>})
}
return (
<TabView
tabs={tabs}
/>
)
It's starting to get complicated to use, and we can do better. If we change my <TabView>
component, we can make a more dev-friendly component which is used like that :
<TabView>
<Tab label="First tab">
<p>My first tab content</p>
</Tab>
<Tab label="Second tab">
<p>My second tab content</p>
</Tab>
{
displayThirdTab && (
<Tab label="Third tab">
<p>My third tab content</p>
</Tab>
)
}
</TabView>
V2 – Not so difficult to code, much easier to use
The difficulty with the above component lies in the fixed part. We need to display only a part of the children.
To do this, we start by creating a "ghost-component" called <Tab>
which will render nothing
const Tab = ({tabs}) => {
//Rendered in TabView component
return null
}
With typescript, we can specify the props we need to use them in <TabView>
Then, we will write the base of the <TabView>
component.
const TabView = ({children}) => {
const [selectedTabIndex, setSelectedTabIndex] = useState(0)
const tabsInfo = []
const tabsContent = []
//TODO : Parse children
return (
<div>
<div className="header">
{tabsInfo.map(({label}) => (
<p>{label}</p>
))}
</div>
<div className="content">
{tabsContent[selectedTabIndex]}
</div>
</div>
)
}
You can see two arrays :
-
tabsInfo
will contain all the tabs headers data (just a label in our case) -
tabsContent
will contain all the<Tab>
componentschildren
props
We now need to parse the children
prop to fill our arrays.
To do this, we add a function called parseTab
const parseTab = (node) => {
//We extract children from the <Tab> props
tabsContents.push(node.props.children)
//We extract label from <Tab> props
tabsInfo.push({ label: node.props.label })
}
We just have to call it for each node in children with the React.Children.map
React.Children.map(children, parseTab)
Here we are, our final <TabView>
component
const TabView = ({children}) => {
const [selectedTabIndex, setSelectedTabIndex] = useState(0)
const tabsInfo = []
const tabsContent = []
const parseTab = (node) => {
//We extract children from the <Tab> props
tabsContents.push(node.props.children)
//We extract label from <Tab> props
tabsInfo.push({ label: node.props.label })
}
React.Children.map(children, parseTab)
return (
<div>
<div className="header">
{tabsInfo.map(({label}) => (
<p>{label}</p>
))}
</div>
<div className="content">
{tabsContent[selectedTabIndex]}
</div>
</div>
)
}
Top comments (0)