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 :
- 
tabsInfowill contain all the tabs headers data (just a label in our case)
- 
tabsContentwill contain all the<Tab>componentschildrenprops
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)