Author avatar

Chris Dobson

Component Composition in React

Chris Dobson

  • Jun 18, 2019
  • 7 Min read
  • 442 Views
  • Jun 18, 2019
  • 7 Min read
  • 442 Views
Web Development
React

Introduction

When building an application using React, it is usually the case that developers will want to re-use component code. Developers new to React will often, instinctively, try to achieve this using inheritance. However, it is nearly always better to use React's composition model.

A good example of components that often need to re-use component code is tabs which show different content when selected. This guide will start with a set of tabs that re-use no code whatsoever and build specialized components, container components, and a combination of both to achieve re-use through composition.

Tabs

A simple way to build a component containing tabs is to do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<ul className="nav nav-tabs">
  <li className="nav-item">
    <button
      className={`nav-link${selectedTabIndex === 0 ? " active" : ""}`}
      onClick={() => setSelectedTabIndex(0)}
    >
      {"Tab 1"}
    </button>
  </li>
  <li className="nav-item">
    <button
      className={`nav-link${selectedTabIndex === 1 ? " active" : ""}`}
      onClick={() => setSelectedTabIndex(1)}
    >
      {"Tab 2"}
    </button>
  </li>
</ul>
<div className="tab-content">
  {selectedTabIndex === 0 && (
    <>
      <h2>{"Tab 1"}</h2>
      <p>{"Some content for the first tab"}</p>
    </>)}
  {selectedTabIndex === 1 && (
    <>
      <h2>{"Tab 2"}</h2>
      <p>{"Some content for the second tab"}</p>
    </>)}
</div>
javascript

The ul element contains the UI for each tab separately and the content is displayed in the div element at the bottom. This is OK; however, when new tabs are required, code will need to be copy/pasted and if any of the elements that are common across tabs or content need to be changed then they will have to be changed for every tab, which is not ideal. To make this easier, the tab components can be composed using one or a combination of both of the following patterns.

Specialized Components

A specialized component is a generic component that accepts props that are used to render a specialized version. A specialized component for the tab UI looks like this:

1
2
3
4
5
6
7
8
const TabSpecialized = props => (
  <li className="nav-item">
    <button 
      className={`nav-link${props.selected ? " active" : ""}`} 
      onClick={props.onSelect}>
      {props.text}
    </button>
  </li>);
javascript

This component takes props of text, selected, and onSelect to define the text to show on the tab: a boolean indicating whether the tab is selected and a function to call when the tab is clicked. This tab component can then be consumed like this:

1
2
3
4
5
<TabSpecialized
  text="Tab 3"
  selected={selectedTabIndex === 2}
  onSelect={() => setSelectedTabIndex(2)}
/>
javascript

Therefore, if the display of the tab needs to change, this change is made only once in the SpecializedTab component which will then be reflected in all of the tabs.

Similarly, a specialized component can be used for the tab content:

1
2
3
4
5
const TabContentSpecialized = props => (
  <>
    <h2>{props.header}</h2>
    <p>{props.paragraph}</p>
  </>);
javascript

This component takes props of header for the header to display and a paragraph of text. The component is consumed like this:

1
2
3
4
<TabContentSpecialized
  header="Tab 3" 
  paragraph="Some content for the third tab"
/>
javascript

This will render the header inside an h2 element, followed by the paragraph.

As with the tab component, if any changes need to be made to how either the header or paragraph are displayed then this change need only be made in the TabContentSpecialized component.

Container Components

In many cases, a specialized component will suffice, however, a drawback to using this pattern with these tab components is that it assumes that all components will be displayed in the same format and, indeed, that it is known how the content for every tab in the application will be displayed. For the actual tab components, this may well be correct. However, for the content, it probably will not be. The specialized content component takes props of strings for a header and a paragraph; if anything else needs to be rendered then either optional props can be added to the component to cover all cases that are known, or a container component can be used. Adding props for all cases, again, assumes that all cases are known and can also make the component code very complex; therefore a container component is the better option.

All React components have a special children prop so that consumers can pass components directly by nesting them inside the jsx. This prop can be used by a tab content component to accept the actual content without needing to know anything else about it.

A version of the tab content component as a container looks like this:

1
2
3
4
const TabContentContainer = props => (
  <div className="tab-content">
    {props.children}
  <div />);
javascript

This component simply renders the child components inside a div which is used to ensure that content is shown with a standard style and is consumed like this:

1
2
3
4
5
6
<TabContentContainer>
  <div className="tab4-content">
    <img src={logo} className="App-logo" alt="logo" />
    <p>This tab can contain anything.</p>
  </div>
</TabContentContainer>
javascript

Everything nested inside the TabContentContainer element is passed as the children prop and is rendered by the content component meaning that, as the text says, the tab can now contain anything.

Combining Specialized and Container Components

In the case of the tab components, a combination of both specialization and containers is probably the best pattern to use. This way, each tab and content can show text for a header in the same style but also can use the children prop to define specific content.

The code for a combined tab component looks like this:

1
2
3
4
5
6
7
8
9
10
const Tab = props => (
  <li className="nav-item">
    <button 
      className={`nav-link${props.selected ? " active" : ""}`} 
      onClick={props.onSelect}
    >
      {props.text}
      {props.children}
    </button>
  </li>);
javascript

In addition to the props for the specialized tab component, there is now a children prop which allows the tab to display any extra content. So, using this component to add an image to the tab can be done like this:

1
2
3
4
5
6
7
<Tab 
  text="Tab 4" 
  selected={selectedTabIndex === 3} 
  onSelect={() => setSelectedTabIndex(3)}
>
  <img src={logo} className="App-logo" alt="logo" />
</Tab>
javascript

The content component looks like this:

1
2
3
4
5
const TabContent = props => (
  <>
    <h2>{props.header}</h2>
    {props.children}
  </>);
javascript

This component has added a header prop to the TabContentContainer component above and can be consumed like this:

1
2
3
<TabContent header="Tab 4">
  ...
</TabContent>
javascript

The header will then be displayed followed by the children.

Conclusion

React's powerful composition model allows developers a relatively easy way to re-use component code. This guide has used composition patterns to create tab components that share code to provide a consistent UI while being flexible enough to allow different content.

A sample application using composition to build tabs can be found here.

12