DEV Community

Cover image for Component Composition in React
Kevin Farrugia
Kevin Farrugia

Posted on • Originally published at imkev.dev

Component Composition in React

Cross-posted from https://imkev.dev/component-composition

One of the most misunderstood design patterns in a React application is the render props pattern and component composition. While this isn't a new or novel approach and has been promoted since at least 2017, five years later I still encounter opportunities where it should have been used instead of an inheritance hierarchy. Component composition is the technique of combining different modular components to construct another component.

Render Props

"But almost all components are made up of sub-components, so what's so special about component composition?" I hear you say. I distinguish between a regular component and component composition by passing one or more of the sub-components as props to the parent component. These props are known as render props and the most commonly used render prop is the children prop which is included in React by default.

Let's take a look at an example.

import Title from "./Title";

export default function MyComponent({ title, text }) {
  return (
    <div className="container">
      <Title title={title} />
      <p class="text">{text}</p>
    </div>
  );
}

export default function App() {
  return (
    <div className="app>>
      <MyComponent
        title="Random title #1"
        text="Lorem ipsum..."
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Inheritance Demo

The component above might look fairly ordinary. A component MyComponent renders a div element and within it, there are two child elements. One being the <Title> component and the other being a <p> element. MyComponent receives two props, the title and the text component, which it outputs or passes onto the sub-component.

Let's see the same component using the component composition pattern.

export default function MyComponent({ children }) {
  return <div className="container">{children}</div>;
}

export default function App() {
  return (
    <div className="app">
      <MyComponent>
        <>
          <Title title="Random title #1" />
          <p className="text">
            Lorem ipsum...
          </p>
        </>
      </MyComponent>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Component Composition Demo

In this example, the role of MyComponent is reduced to creating a div element and placing the children prop within the div. The parent component which calls MyComponent is responsible for creating the <Title> and <p> elements. The generated HTML in these two examples is identical.

Single-responsibility principle

When I was still at University studying computer science, amongst the many practices we were taught, there were the SOLID principles. Without going into the merits of the SOLID principles, the S in SOLID stands for the Single-Responsibility principle and states (paraphrasing) that a class or function should only have one reason to change. A class or function should only have one role. I like that. It makes it easier to understand, easier to debug, and makes your code more portable.

The component composition pattern helps enforce this practice as the role of MyComponent in the example above is to only create the div and place the children in the correct place. The role of App is to construct the composition of different components required to build the module. Contrary to the first example, MyComponent is not responsible for choosing the order of the <Title> and <p> elements, and if you would want to change their order, you would need to change MyComponent. This violates the Single-Responsibility principle.

In practice

The above example is very simple and you are unlikely to encounter this scenario in a real-world environment. But the concept could be applied to any component structure.

In addition to displaying and outputting HTML, one of the more common tasks of a React component is to fetch data from a store or an API. Let's compare inheritance and component composition using an example where we are fetching a list of users and then displaying these in a <ul>.

export default function UserList({ quantity }) {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch(`${API_URL}${quantity}`).then(async (response) => {
      if (response.ok) {
        const { results } = await response.json();
        setUsers(results);
      }
    });
  }, [quantity]);

  return (
    <div className="container">
      {users && Boolean(users.length) && (
        <ul className="list">
          {users.map((n) => (
            <li key={n.login.username} className="item">
              <UserCard
                username={n.login.username}
                city={n.location.city}
                profilePicture={n.picture.thumbnail}
              />
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default function App() {
  return (
    <div className="app">
      <UserList quantity={3} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Inheritance Data Demo

The UserList component receives a quantity prop indicating the number of items to retrieve from the API. Once the component is mounted, it will make a request, populate the result in the state, and then display a list of UserCard sub-components inside a <ul>.

Let's take a look at the same application if it were following the component composition pattern.

export default function Users({ quantity, children }) {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch(`${API_URL}${quantity}`).then(async (response) => {
      if (response.ok) {
        const { results } = await response.json();
        setUsers(results);
      }
    });
  }, [quantity]);

  return children({ users });
}

export default function App() {
  return (
    <div className="app">
      <Users quantity={3}>
        {({ users }) => (
          <div className="container">
            {users && Boolean(users.length) && (
              <ul className="list">
                {users.map((n) => (
                  <li key={n.login.username} className="item">
                    <UserCard
                      username={n.login.username}
                      city={n.location.city}
                      profilePicture={n.picture.thumbnail}
                    />
                  </li>
                ))}
              </ul>
            )}
          </div>
        )}
      </Users>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Component Composition Data Demo

The App component now renders a Users component. This component is solely responsible for fetching the users from the API and returning them as a prop to the children using the return statement return children({ users }). Any child component of Users will have access to the users as a prop. The App component iterates through the users it receives and creates the <ul>.

The latter approach allows you to separate fetching data from displaying it. If a change request comes in that requires the data to be filtered before being displayed, you immediately know that you do not need to do any changes to the Users component as the change request does not require changes to the fetching of data. Demo

Multiple Render Props

While in many cases you can use the children prop included in React, in some cases you may need to place multiple sub-components that will not be rendered beside each other in the DOM. In these cases, you may define further render props similarly to how you would assign an ordinary prop. The only difference is that you pass a component.

export default function MyComponent({ headerFn, children }) {
  return (
    <>
      <div className="header">{headerFn}</div>
      <hr />
      <div className="container">{children}</div>
    </>
  );
}

export default function App() {
  return (
    <div className="app">
      <h1>Component Composition</h1>
      <MyComponent headerFn={<Title title="Random title #1" />}>
        <p className="text">
          Lorem ipsum...
        </p>
      </MyComponent>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this simple example, we have added headerFn prop to MyComponent and passed <Title title="Random title #1" /> to it. MyComponent is only responsible for the DOM structure and placing the correct CSS classes, while App is responsible for defining the components to be rendered.

Conclusion

The component composition pattern can simplify your applications, making the code more portable, maintainable, and DRY. Once you become accustomed to this pattern, it is easy to apply it in almost any situation you would have previously used inheritance.

I hope this helped convince you to make use of component composition more often. If you're not yet convinced, ReactJS docs go as far as to say that they havenโ€™t found any use cases where they would recommend creating component inheritance hierarchies and Michael Jackson has a fantastic video (one of many) on this topic Never Write Another HoC.

Thank you for reading. Have a good one! ๐Ÿ‘‹

Image credits: Photo by Ricardo Gomez Angel

Top comments (0)