loading...
Cover image for Thinking in React: Component Composition

Thinking in React: Component Composition

bouhm profile image Brian Pak Updated on ・3 min read

When starting out with React, I realized it was easy for me to get carried away with creating too many components. This lead to deeply nested structures which made it a pain to pass props all the way down. I was creating components that were too specialized to be as versatile or reusable as they could be.

What is Composition?

In React, composition is a natural pattern of the component model. It's how we build components from other components, of varying complexity and specialization through props. Depending on how generalized these components are, they can be used in building many other components.

Reducing Component Nesting With Composition

Let's say we are building a component AlbumCard. Each album card will have various information about the album including the name, songs, album cover, artist, genres, etc.

A simplified mock-up may look like this:

const AlbumCard = props => {
  return (
    <div className="card">
      <img src={props.albumCoverUrl} />
      <AlbumInfo {...props} />
      ...
    </div>
  )
const AlbumInfo = ({ title, artist, genres, songs }) => {
  return (
    <div>
      <h1>{title}</h1>
      <h2>{artist}</h2>
      <ul>{genres.map(genre => <li>{genre}</li>)}</ul>
      <SongContainer songs={songs} />
    </div>
  )
const SongContainer = props => {
  return (
    <div>
      {props.songs.map(song => <SongCard {...song} />)}
      ...
    </div>
  )
const SongCard = props => {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.songLength}</p>
    </div>
  )

We can do better here to reduce the nested structure by using props.children and by handling specialization through props.

const AlbumCard = ({ albumCoverUrl, title, artist, genres, songs }) => {
  return (
    <Card title={title} img={albumCoverUrl} >
      <h2>{artist}</h2>
      <ul>{genres.map(genre => <li>{genre}</li>)}</ul>
      <div>{songs.map(song => <SongCard {...song} />)}</div>
    </Card>
  )
const SongCard = props => {
  return (
    <Card title={props.title}>
      <p>{props.songLength}</p>
    </Card>
  )
const Card = props => {
  return (
    <div className="card"> 
      {props.img && <img src={props.img} />}
      <h1>{props.title}</h1>
      <div className="card-content">
        {props.children}
      </div>
    </div>
  )
}

While passing in the common property title of the AlbumCard and SongCard as a title prop for Card, we can pass in the rest of the card contents by wrapping them in the Card component we created. These are passed in as props.children.

What are some benefits of this change? We now have code (the Card component) that is more reusable as a result of it being generalized. We create the specialized cards AlbumCard and SongCard by passing props through Card. We made our overall code cleaner by unifying individual components that are not reusable, resulting in a simpler structure that doesn't require passing props down through many components.

It's important to note that this is just a demonstration of a concept and by no means the 'right' way. Depending on your needs, you may compose your components differently.

 

Composition With Higher-Order Components

A higher-order component is another pattern of the React component composition model. At a high-level view, it is just a function that takes in a component and creates a new, "enhanced" component from it.

It can be used when we have multiple components that may share some properties like functionality or data. The first instinct may be to derive those shared properties with inheritance. But as a general practice we don't do inheritance in React; we utilize composition instead.

Reusing our example, let's say we want our SongCard component and AlbumCard to receive data on the ranking of all songs and albums so that we can display something with that data. First, since we're now handling data in these components, we may want to change them into class components.

class SongCard extends Component {
  componentDidMount() {
    fetch(RANKING_API_URL)
    .then(...)
    ...
  }

  receiveRankingUpdate() {
    ...
  }

  ...
}
class AlbumCard extends Component {
  ...

  componentDidMount() {
    fetch(RANKING_API_URL)
    .then(...)
    ...
  }

  receiveRankingUpdate() {
    ...
  }

  ...
}

Instead of having repetitive code, we can make it reusable with composition! We can define a higher-order component like so:

const withRanking = WrappedComponent => {
  return class extends Component {
    ...

    componentDidMount() {
      fetch(RANKING_API_URL)
      .then(...)
      ...
    }

    receiveRankingUpdate() {
      ...
    }

    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

And then compose our new SongCard and AlbumCard components using the HOC:

import SongCard from './SongCard'
import AlbumCard from './AlbumCard'

...

const SongWithRanking = withRanking(SongCard)
const AlbumWithRanking = withRanking(AlbumCard)

The SongCard and AlbumCard components do not get modified in any way, they are simply being wrapped with additional data and props passed in.

So remember, if the code has repetition and nesting woes, rethink in React and just compose.


References

Posted on by:

Discussion

pic
Editor guide
 

Thank you so much! But what I do not like about your example is that the request to the API is still done twice. I think a Context API approach would be better to share the data with one single request.