Decompose React children to improve DX
Tags: react, dx, tabs, children
Published: 2022-03-13 20:39:58


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 :

```jsx
<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 :

```jsx
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 : 

```jsx
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 :

```jsx
<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

```jsx
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.

```jsx
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>` components `children` props


We now need to parse the `children` prop to fill our arrays.
To do this, we add a function called `parseTab`

```jsx
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`](https://fr.reactjs.org/docs/react-api.html#reactchildrenmap)

```jsx
React.Children.map(children, parseTab)
```

Here we are, our final `<TabView>` component 

```jsx
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>
    )
}  
```