Reusability in React through Composition

3 minutes to read

04.11.2020

React
Best Practices
Refactoring

I recently identified a common anti-pattern that plagues a lot of React code. This pattern starts off as innocent, as many anti-patterns do. This pattern seems like the simplest solution to the problem at hand but leads to the death of reusability by one-thousand paper-cuts. This pattern is props.

Now before you get out the pitchforks, hear me out. Props are one of the handful of tools we have on our React tool-belt for passing around data. It is often taught that props are the way to get data from one component to another. I would agree with this in fact: props are the way to get data between components; however, props are often abused into tasks they are not suited for.

Optional Elements

One of these prop anti-patterns is using props to indicate optional elements. I will use the common situation of a form to illustrate. Note: This example is almost verbatim out of a real codebase I have worked on.

Let's start with the not so great API that we started with:

<FormElement label="Email" name="email" value={/* */} onChange={/* */} />

This was the first version of our FormElement component. It seemed straight forward enough. We wanted to have an easy way to encapsulate the idea of a input with an associated label.

Quite quickly, the scope of the component began to grow:

<FormElement
  label="Email"
  name="email"
  value={/* */}
  onChange={/* */}
  isErrored={/* */}
  helperText="..."
/>

And grow...

<FormElement
  label="Email"
  name="email"
  value={/* */}
  onChange={/* */}
  isErrored={/* */}
  helperText="..."
  rightInputIcon={/* */}
  leftInputIcon={/* */}
  input={Input}
  required
/>

The straw that broke the camels back was when we ran into a situation where we needed to move the helper text, but only in some situations. How do we handle that? A new prop called upperHelperText? A prop called helperTextPosition? This had to stop.

Composition to the rescue!

Like many problems, the solution is to break the problem into many smaller problems. We can do this by using the other main method of passing information between components: children

.

Let's go ahead and dream up a nicer API that still handles all the problems we have discovered:

<FormElement isErrored={/* */}>
  <Label>
    Email <Required />
  </Label>

  <InputGroup>
    <InputRightElement>
      <Icon name="search" />
    </InputRightElement>
    <Input name="email" value={/* */} onChange={/* */} />
  </InputGroup>

  <HelperText>Email</HelperText>
  <FormError>Email is required</FormError>
</FormElement>

This API is pretty much directly taken from the great work by @thesegunadebayo on Chakra UI.

In order to move our helper text to be above the input, we don't need another prop! We can just... move it!

<FormElement isErrored={/* */}>
  <Label>
    Email <Required />
  </Label>
  <HelperText>Email</HelperText>
  <InputGroup>
    <InputRightElement>
      <Icon name="search" />
    </InputRightElement>
    <Input name="email" value={/* */} onChange={/* */} />
  </InputGroup>

  <FormError>Email is required</FormError>
</FormElement>

Here are some advantages of this new API:

  • You can insert new elements anywhere you want. Requirements change? Throw a special case right on in there!
  • You can disclude elements you don't need for this situation. Perhaps you don't want to display errors in this field because they are being handled somewhere else in the UI.
  • You can reorder elements easily.

Now that we have the more flexible API thought out, some may ask "How am I supposed to migrate my entire codebase to use this new pattern?"

Interconnectivity

These components are now nice and separate, but it is possible that they still need to pass data between them. You have two options for this situation: React.cloneElement, and React.createContext.

If your children are required to not be nested deeply, then you can get away with cloneElement; however, this is a dangerous assumption because you limit the flexibility of the resulting API. Sometimes it is the correct choice, and is also entirely internal to the component and can be more simply refactored later.

Using createContext is more flexible but also more complicated. I would generally prefer the context approach as it allows for the consumer to insert layers of nesting without breaking your data-passing. These sorts of errors are hard to detect unless you know the implementation of the component which inherently breaks encapsulation.

Migration Path

The key to refactoring props into composable components is the fact that the prop API is more limited than the component API. This means that we can implement the component API and then replace the implementation of our original prop-based component using our new components internally. Let's see how that would work:

  • Start off with a prop-based API we want to refactor
  • Refactor this component to use a component-based API
  • Create an identical copy of our original prop-based API but this time, implemented using our new small components. This allows for backwards compatibility.
  • Export both the old API (newly implemented internally) temporarily, and our new API (the small composable components).

This means we can start using the new API, thus avoiding adding to the problem, and allow us to maintain our old API working for the time being. The important next step is, every time you encounter the old API while you are working, you must refactor to replace it with the new API. Once all the old instances of the API are removed (you may just go through and swap them out when the number gets low, or if you have extra time to refactor), you can then delete the temporary export.

Congratulations! You now have a reusable, composable API.

Takeaways

You can use this pattern in many places and situations. Here's my rule of thumb:

If a component has multiple constituent parts, if any are optional, they should be optionally included as children, not optional props.

Avoid props that affect which elements are rendered. Leave that up to the consumer of the API. Or, create a "default implementation" and export both that and the constituent parts.

Other Articles You May Enjoy

Learning to "See the Boxes"

An important skill for designers is to learn to see beneath the UI and understand the boxes that power our apps.

Read more1 min. read

Site Revamp with Chakra and Gatsby

Introducing my new personal website, built using Gatsby and Chakra UI!

Read more1 min. read

Hello!

About me
I'm Adrian Aleixandre, an engineer and designer in Fargo, ND. Right now I'm building web apps at Bushel.

My vision
I am passionate about building UX-research backed products in autonomous cross-functional teams.

Interests
My favorite technical tools are React, Elm, and Elixir. I love me a steaming latte or a milk stout.

Contact
Drop me a line at adrian.aleixandre@gmail.com or on Twitter @_aaleixandre

Adrian Aleixandre • 2020

Made with in Fargo, ND