Atomic Architecture

09/25/2015

As we posted previously, the Craftsy engineering team is busy working on version 2.0 of our site design, code architecture, and systems infrastructure. I want to talk about some of our decisions around the first two and how we are approaching them on our front end team. About a year ago we introduced React into our JavaScript stack and had slowly been incorporating it across our site so we knew we wanted to continue using React - or at least something like it (spoiler, we stuck with React). One of the first questions we wanted to answer was how to structure the components: what boundaries exist between them, when do we create a new one vs. extend an existing one, and how are user interactions defined. At the same time, our UX team was busy answering their own set of questions around how they are going to approach page design, HTML structure, and CSS development. Thankfully we both arrived at the same conclusion: atomic design. For many on our team this is a new way of creating design patterns and is a big jump from our old process of starting new pages and features at the root page level.

Atomic design has a lot of strong proponents in the community and a lot of information is readily available. There is less information around how to write and structure modular code around the same pattern; it seems to me this is something a UX or design team will pick up and force their developers to find a way to make it work. In our case everyone is onboard and we want to make sure everything needed by an atom, organism, or template lives side by side. We took a few weeks to create some proof of concepts, came close to breaking some promises we made to our UXers, and have arrived at a structure I call Atomic Architecture.

Before we started proving the idea we needed to know what business and technological requirements we had to meet. One of the main reasons we decided to do this site-wide rewrite is because we’d reached a point where fixing a bug in one place could easily create one in a different place. We’re putting a very strong emphasis on unit and functional testing and these must be easily created, run, and maintained. Third, we knew breaking changes would be introduced to components in the future as we add features so we need some kind of semantic versioning at a component level. The last goal is a dynamic style library to organize all of the components; allowing UX, front-end, and product to quickly view existing pieces and their configuration options. Those are the major requirements: Atomic design, avoid unintended side effects, make unit/functional testing as smooth as possible, semantically versioned components, and a dynamic style library anyone can view.

To satisfy these requirements we have chosen to make every single component, from atom to page, its own npm module. This has some really big benefits but certainly introduced quite a few challenges. Every component is independently contained, if it has external needs then those additional components, utilities, or anything else can be listed as a dependency; any of our engineers can change a button and immediately get a definitive report on every component relying on it, all the way to a page level. Using npm’s semantic versioning all components will automatically update to use the latest version of their dependencies except when a major version with breaking changes is published; at that time we can detect what components are using the older dependency and update accordingly. Tests, both unit and functional, live inside their relevant module and can be executed independently of other components’ tests. Lastly, our UX team gets to keep the CSS inside the components, no more accidentally targeting other elements on a page!

That last statement requires some explanation; at first glance it sounds great, but we quickly realized some promises were made before proving all of them true. We told our designers they can use the CSS preprocessor of their choice (we went with SCSS), that the CSS they create for one component will not interfere with other components, and we can support multiple versions of the same component on a single page. All transparently to our designers. Integrating SCSS preprocessing isn’t a big deal: you just transpile at build time. However, our original plan for non-interfering CSS was to wrap the SCSS statements in a component class, something like

.FooComponent {
    button { color: white; }
    font-size: 1.6rem;
}

See the Pen ZbBEgQ by Chandler (@chandlerprall) on CodePen.

This is something we obviously didn’t think all the way through, and it bit us hard on a Friday afternoon. It was a realization that one of our front-end developers and myself independently came to at almost the same moment. The approach that we had promised our designers falls apart on two counts. First, if I put any component inside another component, the parent’s CSS still cascades down to the children. Second, this doesn’t support different versions of CSS on the same page, as FooComponent v1 would fight for precedence with FooComponent v1.1. That Friday we took a hard look at our options and came up with three choices: ignore the problem and see if this is a real problem for the designers, use Radium or something similar and keep our styles in the React components themselves, or perform AST transforms on both the CSS and JSX to automagically link the two together. Ignoring the problem wasn’t really viable without getting myself lynched. Including the styles inside the components has gained a lot of traction lately with projects like Radium and react-style, and there is a growing unrest in our community around CSS in general as @vjeux demonstrates in one of his presentations. Transforming ASTs, our last option, didn’t seem to be a path followed by anyone and we would have to write a lot of code ourselves, but it had the benefit that it would allow of to keep all of our promises. So the two of us each took one solution and put together a proof of concept over the weekend. On Monday morning we compared the results and decided to pursue the AST transforms for a few reasons.

1) UX can still uses SCSS - or any other CSS preprocessor of their choice - there’s nothing new to learn

2) It maintains the correct separation of concerns; a component should keep track of its own code, styles, and tests, not know or care what other components look like.

3) It’s cleaner. Admittedly this is a personal preference; I don’t like the look or feel of including styles in components as it gets cluttered and harder to find what I’m looking for.

To do the transforms we have created encapsulate, which takes CSS, JSX, and a class name, automatically adding that class to every CSS selector and JSX tag. Given a class name of “FooComponent-1-0-0” encapsulate will transform this CSS and JSX

<section>
    <h1 className={this.props.headerClass}>Hello World</h1>
    <p className=“smallText”>This is some text.</p>
</section>

See the Pen yYVLwy by Chandler (@chandlerprall) on CodePen.

into

<section className=“FooComponent-1-0-0”>
    <h1 className={this.props.headerClass + “FooComponent-1-0-0”}>Hello World</h1>
    <p className=“smallText FooComponent-1-0-0”>This is some text.</p>
</section>

See the Pen ojYNOG by Chandler (@chandlerprall) on CodePen.

We’ve introduced this into our build process so it is completely transparent to our engineers as we work on components. Since all of our components are separate npm modules we can apply this transform at a module level by extracting the component name and version from its package.json. In this way we can safely write any style and know it cannot affect other components. We are still in the early phases of writing our components so encapsulate probably doesn’t account for every possible CSS selector or way of specifying className in JSX, but we are adding to it as we find new patterns. Solving style scoping this way has been the biggest hurdle so far, really fleshing out and validating the atomic architecture approach.

 

Comments (0)

The comments to this entry are closed.