Back

Harness the Power of Component Composition in React

Harness the Power of Component Composition in React

React is well known for its component-based architecture and component reusability, features that help you build the UI of your app. This approach becomes counter-productive when you build more dynamic components that are deeply nested with their data coming from higher up in the component tree. The good news is React has an in-built solution called “component composition,” as this article will show.

Component composition is an essential concept in React that refers to building bigger or more complex user interfaces by combining smaller or simpler components. As the name says, component composition implies combining bigger components using several smaller components. You can think of it like a jigsaw puzzle. You assemble tiny pieces of the puzzle to give you the final image of the puzzle. Let’s look at an example to better understand how component composition utilizes React.

<select name="languages" id="languages">
  <option>select a language</option>
  <option value="javascript">Javascript</option>
  <option value="python">Python</option>
  <option value="c#">C#</option>
  <option value="php">PHP</option>
</select>;

Yeah, you are right; it’s just your regular old HTML select element. But it’s an excellent example of how the basics of component composition work. The select element wraps around several option elements. These elements work together to achieve the single function of the select element. It is under this principle that the component composition works. To appreciate the problems utilizing the component composition pattern solves, let’s build a blog page with a list of blog posts written by a logged-in author.

-

This is what we will be building. A simple welcome page with a list of blogs. All data used for this page is statically generated. Look at the next code block to understand the structure of the data used

export const userData = {
  name: "Jerome",
  blogPosts: [
    {
      title: "Css Selectors",
      thumbNail: "../public/assets/css-selectors.png",
      content:
        "CSS (Cascading Style Sheets) selectors are a fundamental part of web design and development, allowing you to target and style specific HTML elements on a web page. They are used to define the presentation and layout of web content. Here is a short note explaining CSS selectors",
      date: "18th May",
      readTime: "6 min",
    },
    {
      title: "Asynchronous javascript",
      thumbNail: "../public/assets/javascript-img.png",
      content:
        "Async and await are JavaScript features introduced in ECMAScript 2017 (ES8) that revolutionized the way developers work with asynchronous code. They were specifically designed to address the complexities and callback hell associated with managing asynchronous tasks using traditional techniques like callbacks and Promises.",
      date: "1st July",
      readTime: "10 min",
    },
    {
      title: " useEffect hook React",
      thumbNail: "../public/assets/react-hook.png",
      content:
        "In React, the useEffect hook is a vital part of functional component development. It enables you to perform side effects in your components, such as data fetching, DOM manipulation, or subscribing to external services, all while managing the components lifecycle.",
      date: "23rd January",
      readTime: "5 min",
    },
    {
      title: " useEffect hook React",
      thumbNail: "../public/assets/javascript-img.png",
      content:
        "In React, the useEffect hook is a vital part of functional component development. It enables you to perform side effects in your components, such as data fetching, DOM manipulation, or subscribing to external services, all while managing the component's lifecycle.",
      date: "5th October",
      readTime: "8 min",
    },
  ],
};

In a scenario where the data coming for this page is at a top level, this is how our code will be structured in App.js.

import Navbar from "./Navbar";
import Footer from "./Footer";
import UserPage from "./UserPage";
import "./App.css";
import { userData } from "./userData";
function App() {
  return (
    <div className="container">
      <Navbar />
      <UserPage userData={userData} />
      <Footer />
    </div>
  );
}

export default App;

Let’s see what our UserPage component looks like.

import Blog from "./Blog";
export default function UserPage({ userData }) {
  return (
    <div className="main">
      <h1>Welcome back👋</h1>
      <h2>Your blog posts 📃</h2>
      <div className="blogs-wrapper">
        {userData.blogPosts.map((blog) => (
          <Blog blog={blog} />
        ))}
      </div>
       // {some other UserPage logic...}
    </div>
  );
}

Our UserPage component receives the data from the parent component, displays the user’s name, and passes the data down to the Blog component. This is how the Blog component is structured.

export default function Blog({ blog }) {
  function truncateString(string) {
    if (string.length > 80) {
      return string.slice(0, 80) + "...";
    }
    return string;
  }

  return (
    <div className="blog">
      {children}
      <img src={blog.thumbNail} alt="" />
      <h3 className="title">{blog.title}</h3>
      <p className="content">{truncateString(blog.content)}</p>
      <div className="reactions-date-wrapper">
        <div className="date-readtime">
          <p className="date">{blog.readTime} read</p> |
          <p className="readtime">{blog.date}</p>
        </div>
        <div>
          <span>❤️</span>
          <span>👍</span>
          <span>💬</span>
        </div>
      </div>
    </div>
  );
}

So you can see that our data flows in this direction: App > UserPage > Blog. So, what is wrong with this approach? I mean, it looks just fine, right? Let’s analyze the problems with this approach and how component composition solves them.

Solving prop drilling and unintended re-renders

Our application is structured so that data comes from App down to the Blog component. In React, there can only be a unidirectional data flow; data flows from parent to child components. This becomes a problem when a component serves simply as a passage for data to the component that uses that data. Any mutation in that data would cause a re-render of the component that acts as a passage and any other component below it in the component hierarchy. Let’s revisit our UserPage component for a clearer understanding.

import Blog from "./Blog";
export default function UserPage({ userData }) {
  return (
    <div className="main">
      <h1>Welcome back👋</h1>
      <h2>Your blog posts 📃</h2>
      <div className="blogs-wrapper">
        {userData.blogPosts.map((blog) => (
          <Blog blog={blog} />
        ))}
      </div>
       // {some other UserPage logic...}
    </div>
  );
}

The UserPage component receives userData and passes it down to Blog without consuming any data. Any mutation in userData would result in re-rendering the entire UserPage component. This might not pose a threat considering the size of our application, but imagine we have a large application where this pattern is repeated across our application or imagine there is some heavy logic going on in UserPage forcing a re-render to this component at the price of prop-drilling is quite expensive.

Let’s see how we can solve this issue with component composition. First, let’s modify the UserPage component.

export default function UserPage({ children }) {
  return (
    <div className="main">
      <h1>Welcome back👋</h1>
      <h2>Your blog posts 📃</h2>
      {children}
      ...some other UserPage logic
    </div>
  );
}

This component handles all logic that it is concerned with. Still, it also receives the children prop to help it handle any component that might consume any data that is not important to the functioning of this component. Let’s see how to utilize this component and how it solves our problem of prop drilling and unintended re-renders.

import Navbar from "./Navbar";
import Footer from "./Footer";
import UserPage from "./UserPage";
import "./App.css";
import { userData } from "./userData";
import Blog from "./Blog";
function App() {
  return (
    <div className="container">
      <Navbar />
      <UserPage>
        <div className="blogs-wrapper">
          {userData.blogPosts.map((blog) => (
            <Blog blog={blog} key={blog.title}/>
          ))}
        </div>
      </UserPage>
      <Footer />
    </div>
  );
}

export default App;

Because UserPage receives the children prop, we can call the Blog component inside of it and directly feed it with the data that it needs. This has solved our problem of prop drilling. If, for any reason, userData changes, only the Blog component will re-render and not our entire application.

Building flexible, reusable components

Let’s revisit our Blog component. In a situation where you’d like to create different variants of it depending on your application needs, a common approach will be to pass down props to suit the different needs and variants. Let’s say we want the thumbnail of our blog to be at the bottom in a variant, and in another variant we want the date of publication to be on the left and the reaction buttons on the right, and in another, we want the thumbnail to be completely gone. Let’s add props to our Blog component to try to accommodate all these needs.

<Blog
  blog={blog}
  isThubnailVisible={false}
  thumbnailPosition="bottom"
  datePosition="left"
/>;

Now, our component is being fed several props to fit its needs. What if we have another need? We’ll tweak the component again and pass down props for the required change. Also, consider the amount of logic going on in our Blog component to ensure it functions properly. Although reusable, our component is rigid and not so flexible to modification and scaling. Let’s fix this issue with component composition.

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

We refactor our Blog component to just accept the children prop. Let’s see how to use this component.

import Navbar from "./Navbar";
import Footer from "./Footer";
import UserPage from "./UserPage";
import "./App.css";
import { userData } from "./userData";
import Blog from "./Blog";
import BlogContent from "./BlogContent";
import BlogThumbnail from "./BlogThubmnail";
import BlogTitle from "./BlogTitle";
import BlogDateReactionWrapper from "./BlogDateReactionWrapper";

function App() {
  return (
    <div className="container">
      <Navbar />
      <UserPage>
        <div className="blogs-wrapper">
          {userData.blogPosts.map((blog) => (
            <Blog>
              <BlogThumbnail img={blog.thumbNail} />
              <BlogTitle>{blog.title}</BlogTitle>
              <BlogContent>{blog.content}</BlogContent>
              <BlogDateReactionWrapper>
                <div className="date-readtime">
                  <p className="date">{blog.readTime} read</p> |
                  <p className="readtime">{blog.date}</p>
                </div>
                <div>
                  <span>❤️</span>
                  <span>👍</span>
                  <span>💬</span>
                </div>
              </BlogDateReactionWrapper>
            </Blog>
          ))}
        </div>
      </UserPage>
      <Footer />
    </div>
  );
}

export default App;

Our Blog component is now composed of other smaller components that work together to achieve its functionality. Let’s see what the components that compose our Blog component look like.

export default function BlogThumbnail({ img }) {
  return <img src={img} alt="" />;
}
//////
export default function BlogTitle({ children }) {
  return <h3 className="title">{children}</h3>;
}
//////
export default function BlogContent({ children }) {
  function truncateString(string) {
    if (string.length > 80) {
      return string.slice(0, 80) + "...";
    }
    return string;
  }

  return <p className="content">{truncateString(children)}</p>;
}
//////
export default function BlogDateReactionWrapper({ children }) {
  return <div className="reactions-date-wrapper">{children}</div>;
}

With how our component is built, it is easier to create different variants of our component and add new features.

// thumbnail at the bottom of the title
// date and read time on the right
<Blog>
  <BlogTitle>{blog.title}</BlogTitle>
  <BlogContent>{blog.content}</BlogContent>
  <BlogThumbnail img={blog.thumbNail} />
  <BlogDateReactionWrapper>
    <div>
      <span>❤️</span>
      <span>👍</span>
      <span>💬</span>
    </div>
    <div className="date-readtime">
      <p className="date">{blog.readTime} read</p> |
      <p className="readtime">{blog.date}</p>
    </div>
  </BlogDateReactionWrapper>
</Blog>;

// no thumbnail
<Blog>
  <BlogTitle>{blog.title}</BlogTitle>
  <BlogContent>{blog.content}</BlogContent>
  <BlogDateReactionWrapper>
    <div>
      <span>❤️</span>
      <span>👍</span>
      <span>💬</span>
    </div>
    <div className="date-readtime">
      <p className="date">{blog.readTime} read</p> |
      <p className="readtime">{blog.date}</p>
    </div>
  </BlogDateReactionWrapper>
</Blog>;

You can see that we can easily create different versions of our components with relative ease. Adding features and scaling this component would not be a thorn in the flesh with the approach of composing components.

Benefits of component composition

From the problems solved in our mini blog project, you already have an idea of the benefits of building components with the component composition pattern, but let’s have a quick recap on them.

  • Component composition encourages the reusability of your React components. It also ensures you build more flexible components that are easier to add new features to.

  • Another benefit of component composition is the modularity it brings to your components. Components are built self-contained, making testing and debugging easier. Locating issues in your code base becomes easier because each component is built independently.

  • When components are built with component composition, each smaller component is focused on a specific function or responsibility. This encourages single responsibility of our components and keeps them less bloated and focused on one task. It also improves code readability.

  • By composing our components, we bypass the need to pass state and data through prop drilling. This benefits the performance of our application by eliminating unintended renders.

Conclusion

In conclusion, component composition is a way of building bigger or more complex user interfaces by combining smaller or simpler components. This principle helps React developers build complex components by composing smaller components together. Component composition improves reusability and flexibility of components and prevents unintended re-renders, thereby improving the performance of your application.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay