Target is one among many major retailers that adopted ReactJS to build their e-commerce solutions. An increasing number of internal product teams also opted to build their solution on top of this library.

target_com_react

As adoption increased, so did the need to create and publish reusable React components to share across teams, and even contribute back to the community outside Target. Our web UI/UX engineering team, Praxis, laid the foundation for this effort.

This article is the first of a two-part series to discuss the options and considerations for a component library development environment. We will share important lessons learned while exploring the many ways of handling style and themes for reusable components as well as the tradeoffs, such as the importance of automated testing for the components we’ve created. We’ll examine the importance of continuous integration to ensure no one breaks the build. And finally, we will discuss publishing and what we had to consider prior to distribution, whether to an internal repo or public repository for our consumers.

Why Reusable Components?

Building reusable components required thoughtful consideration with some additional refactoring effort. Here are some considerations before deciding if they are worth the work.

The most important factor is consistency. One of our needs is to promote a consistent look, feel, accessibility, internationalization and features. The challenge is to achieve that without demanding developers acquire deep knowledge of these subject matters. Reusable components help create a shared vocabulary among teams since shared components clearly declare suggested terminology for UI concepts. Enforced consistency increases efficiency by avoiding decision fatigue. The fewer decisions that your developers have to make, the faster they can move. Reusable components can also increase efficiency by saving bandwidth. You send fewer bytes down the wire when you can declare a single component that is used in multiple places on a given page.

Reusable components need to enforce a cohesive vision of the overall architecture. This prevents each team from coming up with their own architecture. Nested components provide a clear path toward composing complex applications using small composable building blocks. Components provide a future-friendly foundation for teams to modify, extend and improve over time. This speeds development both in the short term and long term because it avoids starting from scratch on future projects.

As a component library grows in breadth and depth, each project requires less custom work because you have a stronger and richer foundation to build upon. Reusable components are self-contained. They encapsulate a set of concerns in an approachable and reusable way. Developers don’t have to be experts to use a component because it provides a clear public interface. This allowed our team to scale and generate user interfaces with ever richer building blocks. All of these benefits led to faster development. As our component library grew, we increasingly pulled existing components off the shelf and connected them together. This led to much faster delivery than starting from scratch.

Here is an example of how our team was able to pluck the OAuth UI authentication right off the shelf. The entire set of logics that interact with our internal OAuth provider is packaged inside @praxis/component-auth; developers only have to focus on their own domain-specific logics.

OAuthUI

import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import Auth from '@praxis/component-auth'
import { AuthConsumer } from '../Auth/AuthContext'

const headerStyle = {
  display: 'flex',
  backgroundColor: '#26c6da',
  justifyContent: 'space-between',
  padding: 10,
}

const linkStyle = {
  color: 'white',
  textDecoration: 'underline',
}

export default class Header extends Component {
  render() {
    return (
      <AuthConsumer>
        {context => (
          <header>
            <Auth
              onLogin={(err, data) => {
                if (err) return console.error(err)
                context.login(data)
              }}
              onLogout={() => {
                context.logout()
              }}
            >
              {({ login, logout }) => (
                <div style={headerStyle}>
                  <h3>
                    <Link style={linkStyle} to="/">
                      HOME
                    </Link>
                  </h3>

                  {context.isAuthenticated ? (
                    <ul>
                      <Link style={linkStyle} to="/dashboard">
                        Dashboard
                      </Link>
                      <button onClick={logout}>logout</button>
                    </ul>
                  ) : (
                    <button onClick={login}>login</button>
                  )}
                </div>
              )}
            </Auth>
          </header>
        )}
      </AuthConsumer>
    )
  }
}

Components can be tested in isolation to assure liability so when they’re composed together, we only need to worry about integration concerns between components. Our Praxis library shipped with built-in community-recommended testing libraries such as Enzyme (Functional Test), Jest (Unit Test), Cypress (Integration Test). We introduced our developers to different levels of component testing without the heavy cost of setting up the environment for each team and each developer.

Decision Overview

Creating reusable React components involves a number of design decisions. The core decisions centered around audience and technology, dev environment, how to handle documentation, folder structure, styling and theming, automated testing and what types of testing frameworks to buy in (Enzyme, Jest, Cypress, etc.), and, finally, deployment decisions.

Decision 1: Audience – Who is the Consumer?

Start small! It is simplest to serve the audience a single product. In this scenario, our goal is merely to design components that can be used in multiple places within a single app. Our reusable components enforce consistency across pages. Or does the team plan to use these components for multiple projects built by the same team? Maybe to strive for a grander vision of creating a component library for the entire business, or for use across multiple portfolios or business units. Of course, the hardest audience to serve is the general public. Here’s are some of Target’s open-source contributions.

Decision 2: Rigid Versus Flexible

Our users want to make many items flexible or rigid based on context. For example, users will want to change the style, such as colors, padding, size, font and so on. Some product teams may have a radically different look-and-feel use case with a different predefined layout. Finally, perhaps some of our components contain built-in logic but also accept functions as a parameter to override default behavior.

Rigid Flexible
Highly opinionated Loosely opinionated.
Simpler to create More complex to create
Less time to setup More time to setup
Easier to understand Could contains complex logics
Easier to test More difficult to test
Less maintenance More technical debts

GitHub and GitLab provide a wealth of React components. We often face this decision: Should we link to it? Wrap it? Or fork it?

Link

  • Pros: Simplest Approach. No native support. Team has no control over the product roadmap. Examples: Material UI, Styled-Components. Documentation is just a link to the external author in our documents, and either the business or engineering justification for the decision to go with this component.
  • Cons: Can’t significantly modify the look and feel.

Wrap

  • Pros: Abstract away the component and enhance it with additional features or hide features that don’t apply to a specific use case or need to be always default to a certain value.
  • Cons: When changes occur to the underlying component, we can decide when to accept these new versions, as well as whether to expose any new features. We can enhance existing features, hide others that don’t apply, add opinions and abstract away pieces of complexity for our consumers.

Fork

  • Pros: Team will own the maintenance of the component from that moment on.
  • Cons: When the original component gets a new feature, it may not be practical to integrate the feature into our component.

Decision 4: When Should We Add Components to the Main Library?

"A reusable component should be tried out in three different applications before it will be sufficiently general to accept into a reuse library." - Jeff Atwood, cofounder of Stack Overflow

We kept all candidate components in a folder called “reusable”. This provided a clear spot for developers to check for application-specific reusable components. And it gave us a centralized spot to review for components that have met the rule of three. Application-specific reusable components can often be slightly altered to make them useful in a broader audience. Our community provided feedback via git issue log or request for change as we finalized the decision to officially add the component to the library.

Summary

In this first part of the series, we started by asking the important first question: Why are reusable components worth doing at all? We walked through key benefits of reusable component design, including development speed, consistency and performance. Finally, we considered four core decisions.

  • We must choose an audience and start small. Solve the problem for a specific project before try tackling larger audiences like other teams or the general public.
  • Decide if we want to make components that are rigid to enforce consistency or flexible to support broad use cases. (I suggest starting rigid to keep things simple.)
  • Decide whether we want to link, wrap or fork third-party dependencies. (Linking is easiest, forking gives us the most power, and wrapping is a happy medium. All these approaches are useful, so think carefully about which approach gives us the customization and power we need.)
  • Remember the rule of three. Try a component in three different places before we put it in your reusable component library. That way, we can feel confident that it’s well-designed for reuse in many different contexts.

David Nguyen is lead engineer on a team supporting the Agile and Engineering Enablement practice with a focus on product, Lean, Agile and DevOps. He teaches coding classes at Target’s Immersive Learning Center - DOJO. Brian Muenzenmeyer is lead engineer on the Digital - Guest Experience Management team. He’s the highest git committer on the React Praxis framework. Zachary Wolf is senior web developer on the core Praxis team.