/ react

Using the React Children API

One of the things I love about React is its small API surface - it demonstrates how well a library can encapsulate its complexity away from the end user. In fact, for the first 6 months I wrote applications using React, I think the only parts I used were React.Component and ReactDOM.render (and implicitly React.createElement via JSX). Notwithstanding this small API, there are some lesser used functions that are extremely useful when building composable components.

This post will use parts of the React.Children API to build a simple Tabs component. I want a content area where the current tab will be displayed, and a menu area where each tab will have a clickable header that will load the relevant content.

When developing a component I find it useful to think not just about how I want it to look and function, but also about how I am going to use it from elsewhere in my codebase. Thinking explicitly about the interface you are going to present to users of your component can inform your decisions about how to structure your code, and hopefully results in an intuitive API. This is how I would like my Tabs component API to look:

<TabsContainer defaultTab="home">
    <Tab name="home" title="Home">
        Home Content
    </Tab>
    <Tab name="blog" title="Blog">
        Blog Content
    </Tab>
    <Tab name="contact" title="Contact Us">
        Contact content
    </Tab>
</TabsContainer>

A key point here is that we have avoided exposing any of internal details of the TabsContainer, i.e. maintaining the state of which tab is current, updating the current tab. This keeps the usage of the component clean and easy to comprehend.

We'll start with the Tab component, which is a really simple functional component:

const Tab = ({ title, children }) => (
    <div>
        <h1>{title}</h1>
        {children}
    </div>
);

The TabContainer is really where the action happens, we'll start with the constructor:

class TabContainer extends React.Component {
    constructor(props) {
        super();
        this.state = {
            currentTabName: props.defaultTab
        }
    }
}

We have defined our state as having one property (currentTabName) which represents the currently displayed tab name, and we start it off with the value of props.defaultTab. This might look like a violation of the single-source-of-truth paradigm (and it probably is technically), but in general I find it causes no problems to use this pattern for setting initial/default values.

Now we need a render method that will display the appropriate tab. We'll use our first React.Children function to do this, React.Children.toArray. This does the obvious - return an array of children - but it also adds a key to each child. Once we have an array we can use Array#filter to only show children (hopefully only one - but you could check this or enforce name uniqueness) that match the current tab name.

render() {
    const { children } = this.props;
    const { currentTabName } = this.state;
    
    const currentTab = React.Children.toArray(children).filter(child => child.props.name === currentTabName);
    
    return (
        <div>
            {currentTab}
        </div>
    );
}

Great, so we can now render the appropriate tab, but we need a method to let our users switch between them; we need to render a TabMenuItem for each option. Let's make a simple TabMenuItem component that will take two props - tab title and a callback function for when the menu item is clicked.

const TabMenuItem = ({ title, onClick }) => (
    <div onClick={onClick}>
        {title}
    </div>
);

Now we need to render one of these TabMenuItems for each Tab child of our TabContainer component. We'll do this in a renderTabMenu method within the component, which will be called in render.

We're using another React.Children method here - map. This works just like Array#map, but has built in logic to handle the cases where children is either a single element or null. We map each child to a TabMenuItem element, and use child.props to generate the props our TabMenuItem needs. Our final TabContainer component looks like this:

class TabContainer extends React.Component {
    constructor(props) {
        super();
        this.state = {
            currentTabName: props.defaultTab
        }
    }

    setActiveChild = (currentTabName) => {
        this.setState({ currentTabName });
    }

    renderTabMenu = (children) => {
        return React.Children.map(children, child => (
            <TabMenuItem 
                title={child.props.title}
                onClick={() => this.setActiveChild(child.props.name)}
            />
        );
    }

    render() {
        const { children } = this.props;
        const { currentTabName } = this.state;

        const currentTab = React.Children.toArray(children).filter(child => child.props.name === currentTabName);

        return (
            <div>
                {this.renderTabMenu(children)}
                <div>
                    {currentTab}
                </div>
            </div>
        );
    }
}

There are some other useful parts to the React.Children API that can be found in the React Documentation.