If you don't know what are higher-order components (HOC) or render props, or if you are confused about when you should use them, read on.
What is composition?
Composition in OOP is a way to achieve polymorphic behavior and code reuse. Classes can introduce composition by containing instances of other classes that implement the desired functionality. In OOP, there is an often-stated principle, that you should favor composition over inheritance(originating from the famous book Design Patterns).
The problem with inheritance is that you need to know the hierarchy of relationships beforehand. This causes it not to be very scalable. We can also run into problems like God object or Fragile base class.
JavaScript does not support multiple inheritance, so we can get stuck when we need to inherit traits from multiple classes. It is possible to mimic multiple inheritance by using mixins, but using composition is a much cleaner and simpler solution. We could even opt out of using classes and use factory functions:
const steer = {
steer() {
console.log(`steering ${this.name}`);
},
};
const sail = {
sail() {
console.log(`${this.name} is sailing`);
},
};
const run = {
run() {
console.log(`${this.name} is running`);
},
};
function createVehicle(name, behaviors) {
return {
name,
...behaviors,
};
}
const car = createVehicle('Car', steer, run);
car.steer();
car.run();
const amphibian = createVehicle('Amphibian', run, sail);
amphibian.run();
amphibian.sail()
Function composition
The previous example is using object composition, but another type of composition also exists function composition. In mathematics composition is written like so: f(g(x). This means that the function f is composed using the function g of x. In code, we could implement it like this(thanks to currying):
const compose = f => g => x => f(g(x));
compose (x => x * 4) (x => x + 3) (2);
// (2 + 3) * 4
// => 20
This kind of composition is something to be aware of, but it is not that much related to the composition used in React.
Composition in React
This concept can also be used in the world of React components. To reuse code between components, many new React developers would think about using inheritance. But, as stated in React documentation, composition is the preferred choice. These are composition patterns used in React:
- Containment
- Specialization
- Higher-order components
- Render props
Containment
When using a containment pattern, we can utilize the special children
prop that allows us to pass elements directly from a parent component to a child. If there is a need, we have the flexibility of changing the elements that are passed down to a child component. For example, let's take a header component:
// app.jsx
import Header from './Header.jsx';
export default function App() {
return <Header />;
}
// Header.jsx
import Title from './Title.jsx';
export default function Header() {
return <header><Title text="Basic title" /></header>;
}
// Title.jsx
export default function Title({text}) {
return (<h1>{text}</h1>);
}
In the example, Header
and Title
components are bound tightly together. What if we wanted a different implementation of Header
component? Also, it is very hard to test the Header
component without creating an instance of Title
component. Let's see a way to decouple the components using the child
property:
// app.jsx
import Header from './Header.jsx';
export default function App() {
return (
<Header>
<Title text="Basic title" />
</Header>
);
}
// Header.jsx
export default function Header({ children }) {
return <header>{ children }</header>;
}
If you need multiple "slots" to be passed to a component, you can pass multiple components as props:
// app.jsx
import Header from './Header.jsx';
export default function App() {
return (
<Header
title={
<Title text="Basic title" />
}
description={
<Description text="Basic description" />
} />
);
}
// Header.jsx
export default function Header({ title, description }) {
return (
<header>
{title}
{description}
</header>
);
}
Specialization
React uses composition to allow for more specific components to render a more generic one. We can achieve this by configuring the generic component using props:
// Title.jsx
export default function Title({ text }) {
return <h1>{text}</h1>;
}
// FancyTitle.jsx
export default function FancyTitle() {
return <Title text="Fancy title" />;
}
Higher-order components
Before render props, higher-order components were the most popular way to enhance and compose React elements. A higher-order component is a function that takes a component and returns a new and enhanced component. When using HOC we can control the input of the component. This brings many possibilities: for example, we can send something that the component usually has no access to. But it is important to follow the following convention: HOC should pass through props that are unrelated to its specific concern. Let's see this in action:
const enhanceTitle = (TitleComponent) =>
class EnhanceTitle extends React.Component {
constructor(props) {
super(props);
this.state = { additionalTitle: null };
}
componentDidMount() {
fetch('endpointUrl').then((data) => {
this.setState({ additionalTitle: data.title });
});
}
render() {
return (
<TitleComponent {...this.props} title={this.props.title} adittionalTitle={this.state.additionalTitle} />
);
}
};
const BasicTitle = ({ title, additionalTitle }) => (
<h1>
{title}
{additionalTitle}
</h1>
);
const EnhancedTitle = enhanceTitle(BasicTitle);
Render props
With containment, we were passing React components as children
prop. Another pattern called render props is using a prop whose value is a JSX expression. This is very powerful and can be useful for sharing code between components. Let's see an example:
const PersonList = ({ persons, children }) => {
return (
<section className="main-section">
<ul className="person-list">
{persons.map((person, i) => (
<li key={i}>{children(person)}</li>
))}
</ul>
</section>
);
};
const App = () => {
const persons = [
{ name: 'Doris', surname: "O'Connor", highlighted: true },
{ name: 'Barbara', surname: 'Bloggs', highlighted: false },
{ name: 'Ruth', surname: 'Ashley', highlighted: false },
];
const isHighlighted = (person) => person.highlighted;
return (
<PersonList persons={persons}>
{(person) =>
isHighlighted(person) ? (
<b>
{person.name} {person.surname}
</b>
) : (
<span>
{person.name} {person.surname}
</span>
)
}
</PersonList>
);
};
From the example we can see that we are hiding the structure of the data from the PersonList
component, its job is just to show the list. This also means that we can pass any other implementation of person rendering in the future.
This pattern is called render prop because you would originally use prop called render
to pass the JSX expression. But you could also use any other prop. In the example above, we are using the chidlren
prop.
HOC vs render props
HOC and render props patterns are both used for creating cross-cutting logic. When comparing these two, HOC pattern does have more downsides:
- Ensuring all relevant props are passed through to the component.
- Hoisting static methods from the wrapped component.
- Can be problematic to debug.
This is why render props pattern is more recommended than HOC.
Composition patterns vs hooks
Hooks were introduced in 2018 to improve the reuse of state logic between the components. This means that they cover similar use cases as HOC and render props patterns. Since hooks help with reducing the component nesting in the tree, they are the recommended option. So, does this mean that hooks completely replace HOC and render props? Actually, there is still a place for both patterns. When talking about HOC, there are cases when this pattern is better applicable than the other alternatives:
- Composing multiple HOC units.
- Enhancing callbacks and React lifecycle events.
- Intercepting DOM events.
- Context selectors.
Render props also shine in some scenarios:
- Wrapping hooks so they can be used with class components.
- Enabling custom rendering.
Also, with hooks you may fall into re-render issues. When writing custom hooks, It is important to be aware that every state change in a hook, whether it affects its return value or not, will cause the “host” component to re-render.
Conclusion
React has been changing over the years and hooks have become a de-facto standard for cross-cutting logic. But composition and patterns like HOC and render props still have use in modern React applications and they can be a very powerful tool in your toolbox.