Mastery Games

Principles of Strong Components

JavaScript projects start out fun. They begin with a full heart and an empty index.html. But things tend to get messy and unorganized in a hurry. Why does this happen?

A codebase is like a tower of bricks. Each meticulously placed and relying on the strength of those beneath it. Each intended to hold the entire weight of the rest of the tower. One of the main reasons rewrites eventually seem inevitable is because we're not great at building bricks.

The last couple years have been incredible for the entire web dev community. We're collectively on board with the idea that features should be built as a tree of components. Components are the primary building blocks (bricks) of our applications. They're also a primary indicator of the health of a project. If you have great components, you've got a solid shot at keeping your project clean and simple - a state that helps you write new features rather than slows you to a crawl. You most likely don't need to lock yourself in a supply closet for months to hack on that (super risky) rewrite. You probably just need better components.

Using the component() helper that comes with your preferred UI framework is a great start, but it's not enough. There are principles that if followed can make your components useful, composable, maintainable, and reliable. If written well they can become productivity assets rather than a growing liability. So what makes a component strong?

A strong component:

1. is cut at the right boundaries

A component that tries to do too much is guaranteed to introduce too much complexity. One that does too little is hardly worth importing. Developing an intuition around component boundaries takes some time but is worth practicing. Don't get paralyzed trying to get it perfect though either. It's best to look at the mock/sketch/design and just build it as the first name that comes to mind. If you start feeling friction you can always adjust the boundaries or break it up into disparate components.

Some signs the boundary is too broad:

  • You struggle naming it, or its name doesn't really seem to fit what you're using it for
  • It starts taking more than just a few configuration options
  • The component code starts to get large

2. is easy to locate

When inspecting the DOM you should see the components as the elements, rather than a generic tag like <div/> with magic attributes.
For example you should see <Playlist/> instead of <div class="playlist" />.
This makes it easy to know which component you need to jump to and edit to make a change.
React (with dev tools) and Angular 2 do this by default. If you're on Angular 1 you can write components for almost everything, and restrict any directives to elements (restrict: 'E').

The component's name should also be fairly easy to guess correctly and find in the project.

3. contains all its parts

A strong component has its HTML and CSS collocated in the same file with its JavaScript. This brings enormous cognitive benefits.
From the psychological concept of chunking we learn that we can hold up to 6 or 7 related items in our working memory at a time.
If you have to reason about three different files (plus a directory to hold them) per component you'll hit this limit pretty quick.
When your component contains all its parts you can more easily load it into memory as a single entity.
So for example you could work on a CardDetails feature, that contains an Avatar, Tags, Description, and Members components all without feeling overwhelmed (or having to open 15 files).

There are many additional benefits to this principle:

  • the perceived overhead of creating a new component is a lot smaller, so you're more likely to break up large components into smaller ones.
  • the fact that CSS and HTML count towards your component's lines of code will naturally encourages you to write smaller components.
  • ability to apply tree-shaking to unused components means fewer unused styles on the page
  • flexibility to lazy load components with their styles on demand

React already includes its HTML (JSX) in the component file. Angular 1 and 2 can also with ES2015 template literals.

For CSS, Angular 2 comes with a great way to do this out of the box. New tools like Aphrodite make it easy for React/Angular1/Vue etc.

4. avoids side effects

Bugs caused by side effects are some of the hardest to track down. A strong component is good citizens. It avoids modifying state (including global variables) that doesn't belong to it, and avoids writing to or reading from parts of the DOM that are outside of its root element.

5. has few options

A component that needs a lot of configuration options is going to be a pain to maintain and a pain to use.
Keep the component as simple as you can. Needing a lot of options is usually a sign that you're trying to make your component too generic.
Instead, split it up into more specific components. For example instead of having a single User component that tries to represent all the different ways a user can be presented you might be better off with an Avatar, Bio, and UserSummary components.

6. is imported and used directly

Strong components don't rely on magic global strings for rendering. What you import is what you drop into your markup.
This gives your code traceability which is so critical for a healthy codebase. Without this it's not easy to guess how your app is pieced together. It's also really hard to know for sure if you can delete a component.

Rendering what you import is the default behavior in React:

import Playlist from './Playlist';

const MusicPlayer = () => (
 <Playlist/>
);

With Angular you have to be a bit more deliberate about what your components exports, but it's pretty straight forward.

For example instead of adding a magic string selector to your template:

template: `<app>
            <user-avatar/>
          </app>`

You'd import the component and include render it directly:

import UserAvatar from './UserAvatar';

template: `<app>
            <${UserAvatar}/>
          </app>`

This is a big improvement for Angular development, where common pitfalls include:

  • forgetting to import the component before trying to use it in a template
  • using something (and it working!) that was randomly imported somewhere else...until someone changes it and breaks your seemingly unrelated code
  • messing up the special snake-case to camel-case magic string conversion

Here is a component helper that makes this easy in Angular.

Summary

Building strong components takes discipline and practice, but it's worth it. Over time you'll build up a set of very useful components that will help you build features quickly. They'll be self-contained, easy to use, reason about, and maintain. You'll avoid a lot of crazy side effect bugs and won't get confused about how your project is pieced together.

Build strong components. Your codebase and your productivity will thank you.