A blog by Aaron Godin

Creating React Wrappers for Separation of Concerns

Lately I’ve been following a pattern in my design of React applications to apply the basic design principle of Separation of Concerns. Ideally, each component of your React application encapsulates one and only one responsibility (also tied into the “S” from the SOLID design principles).

Let’s start with an example from a real-world application. On a retail site, you might imagine that there is a button next to every product for adding that item to your cart. But how many ways can you add something to your cart? That might surprise you, given the domain complexity of a large retailer.

Given two types of buttons, one for adding to your cart, and one for launching a different experience, how might you accomplish this?

import { Component } from 'react';
import { CartButton } from 'shared-components/cart-button';

class Product extends Component {
  onClickAddToCart () {
    // add the product to cart
  }

  onClickFindInStore () {
    // launch a "find-in-store" experience
  }

  render () {
    const { product } = this.props;
    const { cartActionType } = product;

    // A Product might have much more than a button,
    // but let's go with this for simplicity
    return (
      <CartButton type={cartActionType}
        onClickAddToCart={this.onClickAddToCart.bind(this)}
        onClickFindInStore={this.onClickFindInStore.bind(this)} />
    );
  }
}

The usage of CartButton should give some implication on how it is built. It relies on the type prop to decide which callback to fire given the type. When it’s 'add-to-cart', let’s fire onClickAddToCart(). Same with the 'find-in-store' type.

This kind of callback style approach is already a nice way of decoupling the functionality of the CartButton from what actually consumes it. Often you’ll need to implement different functionality on various experiences for the callbacks in CartButton. The problem here in the eyes of separation of concerns is that a Product is not responsible for how the cart button behaves. In this case, why not encapsulate that functionality in another component?

Wrappers & Connections

One way I’ve been solving this is through wrapper components, which I often call connections. The idea is that I’m connecting a presentational component to the functionality that drives it. The added benefit is that when I build the wrapper in a decoupled way, that functionality is portable from application to application.

Let’s look at what a wrapper might look like.

import React, { Component } from 'react';

class CartConnection extends Component {
  constructor (props) {
    super(props);

    // a map of props we will pass to our child
    this.cartActions = {
      onClickAddToCart: this.onClickAddToCart.bind(this),
      onClickFindInStore: this.onClickFindInStore.bind(this)
    };
  }

  onClickAddToCart () {
    // add the product to cart
  }

  onClickFindInStore () {
    // launch a "find-in-store" experience
  }

  render () {
    // the juicy bits, take note
    const childElement = React.Children.only(this.props.children);
    const originalProps = childElement.props;

    // Object.assign() maps right to left, this is crucial
    const clonedElementProps = Object.assign({}, this.cartActions, originalProps);

    return React.cloneElement(
      childElement,
      clonedElementProps
    );
  }
}

There is nothing wrong with a component that has no DOM elements associated with it. In fact, I try to write more and more React that behaves this way. I believe once you start thinking with this sort of abstraction, your applications and components will be much easier to reason about.

Taking a closer look at the example above, there are a few things worth explaining. First, we create a map of the methods in the CartConnection class to the names of the child element’s props. This could be configurable based on props for CartConnection as well, if you had a need to make the mappings generic enough to be usable for any component (not just CartButton).

Inside of render(), we perform three actions:

  1. Find the child of CartConnection
  2. Get its properties and merge them over the default cartActions.
  3. Clone the element with the new props.

The eventual return of CartConnection is simply a new instance of the element that was passed in as a child, but we’ve added functionality to it.

Usage

An example usage of this connection in the Product component would look like this:

import { Component } from 'react';
import { CartButton } from 'shared-components/cart-button';
import { CartConnection } from 'shared-component/cart-connection';

class Product extends Component {
  render () {
    const { product } = this.props;
    const { cartActionType } = product;

    // A Product might have much more than a button,
    // but let's go with this for simplicity
    return (
      <CartConnection>
        <CartButton type={cartActionType} />
      </CartConnection>
    );
  }
}

The benefits we’ve seen so far are that a Product knows nothing about how the CartButton works, and in this case it doesn’t need to. Since we designed CartConnection to merge its child’s props over the cart callbacks, a different type of Product could always define its own callbacks.

<CartConnection>
  <CartButton type={cartActionType}
    onClickAddToCart={this.onClickAddToCart.bind(this)} />
</CartConnection>

Because of the decoupled nature, any consumer of the CartButton can choose to use it as they see fit, and same with CartConnection. In my work for the last year, I’ve been developing methods like this to share code among many engineering teams throughout a large organization. Sharing code is not easy, but an emphasis in good design is the way to accomplish the reusability and power that React components provide.