Refactoring React - Component Composition and Reuse

I've been doing a lot of React refactoring lately. I've seen some things.

Refactoring React is a series documenting some of the things I've learned about how to write React and make the lives of future developers (or you a week from now) a little easier.

Component composition is one of React's most powerful features. In my experience, however, it's often underutilized and misunderstood. As a React project becomes more complex, it's important to create reusable and composable components.

Common UI Components

Component composition is the ability to combine React components to build other components. A good composable component encapsulates UI, logic, or both in a way that's easy to reuse. Something like a repeated stylistic element is the perfect thing to encapsulate.

Here's a simple react component that renders a <div> with some content. The styled <div> looks like a 'card' with rounded corners and a drop shadow.

Note: I'm totally glossing over the CSS here for the sake of clarity - more on that later
const MyComponent = () => {
  return (
    <div className='card'>
      <h1>A Fancy Title</h1>
      <h3>Subtitle</h3>
      <img src='/img.jpg' />
      <hr/>
      <p>'Body copy'</p>      
    </div>
  )
};

The project has many components that have the same card UI, but every component re-implements the <div> and the styles to create the card. Some developers may have implemented it differently, or a designer may have set the wrong border-radius in a comp. Having multiple implementations of the same thing is error-prone and inefficient. It also creates a lot of work if the design or business logic in the component ever changes.

This is the perfect low-hanging refactoring fruit to put component composition to use, so let's make a reusable Card component!

const Card = (props) => {
  return (
    <div className='card'>
      {props.children}     
    </div>
  )
};

Notice the children prop. It's a special prop that includes all the JSX nested inside our component. It lets us put other components inside our card component, just like you'd put elements inside a <div>.

Putting Card to use in our original component we get:

import Card from '../common/Card';

const MyComponent = () => {
  return (
    <Card>
      <h1>A Fancy Title</h1>
      <h3>Subtitle</h3>
      <img src='/img.jpg' />
      <hr/>
      <p>'Body copy'</p>      
    </Card>
  )
};

Everything inside the <Card> JSX tag get's passed along to the Card component in the children prop. Card wraps children in a styled <div> and returns the result.

Now that we have a basic Card component, we can extend it with helpful features. If the client asks for a new component with an outlined card, it's easy to add.

const Card = ({ outline }) => {
  const outlineClass = outline ? 'outline' : '';

  return (
    <div className={'card' + outlineClass }>
      {children}     
    </div>
  )
};

Now, if we pass the outline prop to <Card> like so: <Card outline>, we'll get a card with an outline. Every place we've used Card will also have access to this extra functionality.

Be careful when extending or editing common components! Changing a component's default behavior will change or break things elsewhere.

If you don't intend to change things globally, it's critical to cover the component with tests. For something simple that just returns DOM elements like our card example, snapshot testing may do the trick. If there's business logic, make sure it's unit tested.

Also: if you find yourself doing lots lots of string stuff to construct classNames, check out classnames on NPM. It'll make that easier.

Abstracting Complexity

Another important use of component composition is abstraction. Let's say we want to add a description list to our card. We get a list of product features from an API, and we want to sort that list based on user input.

Adding those features to our component might look something like this:

import Card from '../common/Card';

const MyComponent = (props) => {
  const { productFeatures, sort } = props;

  if (sort) {
    productFeatures.sort((a, b) => /* sorting function */ );
  };

  const listItems = productFeatures.map(feature => {
    return (
      <React.Fragment key={feature.id}>
        <dt>{feature.name}</dt>
        <dd>{feature.description}</dd>
      </React.Fragment>
    );
  });

  return (
    <Card>
      <h1>A Fancy Title</h1>
      <h3>Subtitle</h3>
      <img src='/img.jpg' />
      <hr/>
      <p>'Body copy'</p>   
      <dl>
        {listItems}
      </dl>   
    </Card>
  )
};
Notice how we sort features and construct the list outside of return(). See my previous post Refactoring React: Cleaning Up Return for details on this pattern.

We just about doubled the number of lines in our component with our description list logic. While it works fine, we can improve things by pulling out the description list into its own component.

const FeatureList = (props) => {
  const { productFeatures, sort } = props;

  if (sort) {
    productFeatures.sort((a, b) => /* sorting function */ );
  };

  const listItems = productFeatures.map(feature => {
    return (
      <React.Fragment key={feature.id}>
        <dt>{feature.name}</dt>
        <dd>{feature.description}</dd>
      </React.Fragment>
    );
  });

  return (
    <dl>
      {listItems} 
    </dl>
  )
};

We can then simplify our component:


import Card from '../common/Card';
import FeatureList from '../common/FeatureList';

const MyComponent = (props) => {
  const { productFeatures, sort } = props;

  return (
    <Card>
      <h1>A Fancy Title</h1>
      <h3>Subtitle</h3>
      <img src='/img.jpg' />
      <hr/>
      <p>'Body copy'</p>   
      <FeatureList 
        productFeatures={productFeatures}
        sort={sort}
      />  
    </Card>
  )
};
I'm explicitly passing the productFeatures and sort props to the FeatureList component for the sake of clarity here. We could skip the prop destructuring and prop drilling by spreading props into FeatureList's attributes like so: <FeatureList {...props} />.

Having a separate, composable FeatureList component has several benefits. First, we've moved logic specific to the feature list into a separate component. A new developer can look at MyComponent and make sense of it without having to understand how the list is being constructed.

We've also created a reusable component that we can use elsewhere on the site. We don't have to solve the list sorting and fragment constructing all over again.

Finally, it's easier to write and maintain tests for MyComponent and FeatureList when they're separate components. In a real project, MyComponent could contain several additional complex sections like the feature list. Testing all the logic and the DOM output in one place gets messy.

A Note on Composable Styles

It's preferable to let composable components fill the space of their container. Setting a specific width, height, margin, or padding in a reusable component may fit your specific need, but doing so limits its reusability. You can handle layout in the parent component, or you could pass a class name as a prop that will apply specific layout properties.

I like to think of my components as mini responsive pages. They should be able to adapt to whatever container I put them in.

Some React component libraries further abstract layout into specific 'layout components'. The Braid Design System has some good examples.

This may not apply to things like buttons, icons, and other elements that don't usually respond to the size of their container. They often have specific sizes - go ahead and set them.