Decompose React children to improve DX

alexandrefauchard

Alexandre Fauchard

Posted on March 13, 2022

Decompose React children to improve DX

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>
        }
    ]}
/>
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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}
    />
)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

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 })
}
Enter fullscreen mode Exit fullscreen mode

We just have to call it for each node in children with the React.Children.map

React.Children.map(children, parseTab)
Enter fullscreen mode Exit fullscreen mode

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>
    )
}  
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
alexandrefauchard
Alexandre Fauchard

Posted on March 13, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related