React Component Composition: Build Better, More Flexible, and More Performant Components

React Component Composition: Build Better, More Flexible, and More Performant Components

ยท

5 min read

In this post, we're going to take a look at component composition in React.

Composition is a style of building components. It involves passing child components as props to a parent component.

The most common example is the children prop. Composition gives you more flexibility because the child components are dynamically populated. Not only that, but it leads to more performant components by leveraging a built-in React rendering phenomenon.

If you use composition, your components will be easier to reason about and easier to maintain in the long term.

Complex Combinations

It's common to build components where you don't know the children ahead of time.

For example, a footer is a generic component that can usually display anything. You might decide to throw in a few links or an email subscription form. But what happens when you need a different footer on different pages?

Making the footer take a children prop allows you to support different styles throughout your app.

function Footer(props) {
  return <div>{props.children}</div>;
}

function About() {
  return (
    <>
      <p>About Page!</p>
      <Footer>
        <p>Look at me! I'm a footer!</p>
        <a href="some-link">I'm a link</a>
      </Footer>
    </>
  );
}

function Home() {
  return (
    <>
      <p>Home Page!</p>
      <Footer>
        <p>Look at me! I'm a paragraph, but there's also a subscription form!</p>
        <form>
          <label>
            Email: <input />
          </label>
          <button>Submit</button>
        </form>
      </Footer>
    </>
  );
}

In this case, we're able to show a different footer if you're on the Home page vs the About page.

Component Slots

Sometimes a single children prop doesn't give you enough flexibility.

You can use component slots in this case to compose multiple children into complex layouts. Let's take a look at the footer again. If we break it up into a top, a left, and a right section, we can make some interesting layouts. Let's say you wanted to put a search bar on the top, the links on the left, and the email form on the right.

The easiest way to do this is to use different props instead of the children prop. We can name the props whatever we want, but in this case let's just name them top, left, and right.

Here's what that layout would look like:

How the footer layout is broken up into a top, right, and left section

And here's the code for it:

// Don't mind the inline styles ๐Ÿ™ƒ
function Footer(props) {
  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <div>{props.top}</div>
      <div style={{ display: "flex" }}>
        <div>{props.left}</div>
        <div>{props.right}</div>
      </div>
    </div>
  );
}

// The implementations for the components are left out for brevity
function Home() {
  return (
    <>
      <p>Home Page!</p>
      <Footer
        top={<Header />}
        left={<Links />}
        right={<EmailForm />}
      />
    </>
  );
}

All we're doing is passing components as props to the footer.

Easy!

Use Case Variants

Composition allows you to build reusable components.

If we look at the first footer example above, there's a Home page and an About page version of the Footer component. In those examples, the footer is hard coded in place. While that's not necessarily a problem, we probably want to reuse at least one of the footers on a different page.

We can create a special variant to handle the basic version of the footer.

function Footer(props) {
  return <div>{props.children}</div>;
}

function BasicFooter(props) {
  return (
    <Footer>
      <p>Look at me! I'm a footer!</p>
      <a href="some-link">I'm a link</a>
    </Footer>
  );
}

function About() {
  return (
    <>
      <p>About Page!</p>
      <BasicFooter />
    </>
  );
}

Performance Benefits

Another benefit of composition is performance improvements.

Any time a component needs to re-render, React will automatically re-render any children. The exception to this is if the child component has the same object reference between render cycles. If the reference is the same then React can safely bail out of the render.

The best part is that React isn't doing anything special.

It's just using JavaScript object references.

const objectOne = {};
const objectTwo = {};
const objectThree = objectOne;

objectOne === objectTwo // false
objectOne === objectThree // true

Even though all three objects are the same, objectOne and objectTwo have different references.

Here's what that means for you in React.

function Random() {
  // This will change every time `Random` re-renders
  return <div>{Math.random()}</div>;
}

function RandomContainer(props) {
  const [, update] = React.useState(0);
  const forceRender = () => update((prev) => prev + 1);

  return (
    <div>
      <button onClick={forceRender}>Re-Render</button>
      {/** This will re-render when RandomContainer re-renders */}
      <Random />
      {/**
        This won't re-render when RandomContainer re-renders
        because the object reference is the same between renders
      */}
      {props.children}
    </div>
  );
}

function App() {
  return (
    <RandomContainer>
      <Random />
    </RandomContainer>
  );
}

The component Random is used twice in RandomContainer. The difference is that the first instance is a direct child while the second instance is passed in as a prop (i.e. composition). When RandomContainer re-renders, React only re-renders the first instance of Random.

This can be really useful if you have child components with expensive render cycles.

Passing in the expensive component as a child can improve performance for free.

Demo

Deciding when to use composition isn't always obvious

It might not even be necessary in some cases. You don't need to reach for this pattern every time or try to change existing components that are working. That said if you learn to use composition strategically it can be a powerful tool in your toolset.

This pattern can help you write better, more maintainable, and more performant components in the long run.

ย