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.
A simple way to build a component containing tabs is to do something like this:
1<ul className="nav nav-tabs">
2 <li className="nav-item">
3 <button
4 className={`nav-link${selectedTabIndex === 0 ? " active" : ""}`}
5 onClick={() => setSelectedTabIndex(0)}
6 >
7 {"Tab 1"}
8 </button>
9 </li>
10 <li className="nav-item">
11 <button
12 className={`nav-link${selectedTabIndex === 1 ? " active" : ""}`}
13 onClick={() => setSelectedTabIndex(1)}
14 >
15 {"Tab 2"}
16 </button>
17 </li>
18</ul>
19<div className="tab-content">
20 {selectedTabIndex === 0 && (
21 <>
22 <h2>{"Tab 1"}</h2>
23 <p>{"Some content for the first tab"}</p>
24 </>)}
25 {selectedTabIndex === 1 && (
26 <>
27 <h2>{"Tab 2"}</h2>
28 <p>{"Some content for the second tab"}</p>
29 </>)}
30</div>
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.
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:
1const TabSpecialized = props => (
2 <li className="nav-item">
3 <button
4 className={`nav-link${props.selected ? " active" : ""}`}
5 onClick={props.onSelect}>
6 {props.text}
7 </button>
8 </li>);
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<TabSpecialized
2 text="Tab 3"
3 selected={selectedTabIndex === 2}
4 onSelect={() => setSelectedTabIndex(2)}
5/>
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:
1const TabContentSpecialized = props => (
2 <>
3 <h2>{props.header}</h2>
4 <p>{props.paragraph}</p>
5 </>);
This component takes props of header
for the header to display and a paragraph
of text. The component is consumed like this:
1<TabContentSpecialized
2 header="Tab 3"
3 paragraph="Some content for the third tab"
4/>
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.
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:
1const TabContentContainer = props => (
2 <div className="tab-content">
3 {props.children}
4 <div />);
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<TabContentContainer>
2 <div className="tab4-content">
3 <img src={logo} className="App-logo" alt="logo" />
4 <p>This tab can contain anything.</p>
5 </div>
6</TabContentContainer>
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.
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:
1const Tab = props => (
2 <li className="nav-item">
3 <button
4 className={`nav-link${props.selected ? " active" : ""}`}
5 onClick={props.onSelect}
6 >
7 {props.text}
8 {props.children}
9 </button>
10 </li>);
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<Tab
2 text="Tab 4"
3 selected={selectedTabIndex === 3}
4 onSelect={() => setSelectedTabIndex(3)}
5>
6 <img src={logo} className="App-logo" alt="logo" />
7</Tab>
The content component looks like this:
1const TabContent = props => (
2 <>
3 <h2>{props.header}</h2>
4 {props.children}
5 </>);
This component has added a header
prop to the TabContentContainer
component above and can be consumed like this:
1<TabContent header="Tab 4">
2 ...
3</TabContent>
The header will then be displayed followed by the children.
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.