Ember Component Design Principles

Tree tops in forest

Framework Components are terrible abstractions and experts are fallible.

Ok, I got your attention. Of course I don’t believe components are bad. But components, just like classes, can be abused. Similar to the old adage of just wanting a banana, but instead, you got a gorilla holding the banana and the entire forest, the same applies to components and how you design your component dependency tree.

Understanding a large system requires multiple mental models and a latticework of knowledge. Without a team understanding of the tradeoffs that exist with wep app development projects, we may be a danger to the system we are trying to develop. Many parts of our applications are prone to logarithmic difficulty. Abstractions in tests can be difficult to refactor over the long term. As a result of learnings from Brian Cardarella, I’ve greatly enjoyed maintaining tests suites while at DockYard. In addition, Sergio Arbeo is a force within DockYard. Apart from his ability to write clear and concise code, learning about fast properties has been insightful and has made each ingredient in our system discernable and self-evident.

However, I feel one topic that, as a company and a community, we don’t talk about nearly enough is component design.

Let’s prime this conversation by talking about DRY. I have always been confused by this term, which was eloquently described by Ed Faulkner in this tweet. What we are really talking about is our ability to read and understand the code. It’s not about saving keystrokes. It’s not about making changes in one place. It’s about readability. On a large app, this becomes even more important since there is no way any of us will understand all parts completely. We need the ability to understand each piece of the app clearly and successfully. We also want the ability to refactor without breaking a sweat that your whole application will break.

So how can we design components that are built for the long term and are understandable in parts?

1. Go Wide, Not Deep

Which tree structure would you prefer assuming each underline (_) is a component and it flows from top to bottom?

Component Tree 1

  _ (top)
_   _

Component Tree 2

_   _ (top)
  _

Probably the second one (although edge cases are abundant). Component Tree 2 only has one child component we need to deal with. As the app becomes more complicated, the API of the components inevitably expands with complexity. Actions, data, and stylistic concerns get buried deep into a component tree, making it hard to reason about what this page is supposed to do. So if you advocate for DRY, then in reality, Component Tree 2 is more desirable even at the expense of a few extra keystrokes and repeating yourself at the top level of the tree.

Ember enables flatter component trees with block syntax and a variety of other initiatives. This helps us all fall into the pit of success and avoid troublesome software maintenance issues due to complicated logic deep in the component tree.

2. Smart and Dumb Components

     _ (1)
    _ _
   _ _ _
  _ _ _ _
 _ _ _ _ _ (15)

Which component in this tree should be the smartest, (1) or (15)? I would hope (1). Component (15) shouldn’t heavily take advantage of dependency injection and should “generally” take what it gets from the outside world and send it back out.

Take a look at these discussions below that layout these points.

Moreover, I would argue as a new developer to a project, I want to easily understand what this tree does. Does this component react to user clicks, what data does it actually need, etc. By making component (1) the orchestrator of the component tree, I can see what data and actions flow in and out of a child component.

3. Testing Smart and Dumb Components

With this understanding, we can move to testing with smart and dumb components. I’m not talking about unit tests. I generally avoid unit tests for components. I’m talking about rendering tests. In Ember, this is easily possible as shown in the testing components docs or in this video about rendering tests.

An ever present concern is that the tests you write will become obsolete over time, so why write them? Or perhaps you feel like moving fast at the expense of stability. However, if you feel this way, I would argue that either one or two things is wrong: The component architecture is messy or the testing apparatus does not allow you to test the things you want very easily. Having a good test suite and component design go hand in hand. If you screw up one, it makes the other harder and vice versa. If you get both right, stability ensues.

If this is an example of a nested “smart” component, testing this logic and state handling is somewhat difficult. What I would rather do for a child component is simply test that it reacts to a click and not have to stub out services or do anything else crafty.

// deeply-nested-component.js
export default class NestedComponent extends Component {
  @service user
  @service music
  @service intl

  // compute complicated logic
  @computed

  @action
  @action
  ...lots of actions and state handling here
}

Moreover, what happens when you to refactor the tree? Lots of churn and moving code around, right? What if this logic was at the top level component and tested there? As HTML, JavaScript, and CSS move around due to better patterns or business requirements, your tests should still pass. You may just have a few simple assertions that need to be moved or deleted. Thus, you aren’t paying the cost of churn.

Overall, state management and actions should be as far up the tree as possible. This will help you maintain software while prioritizing readability and understandability, even though your immediate need is to get just one component working.

Summary

Humans are incorrigibly inconsistent in making judgements with complex information. I read once that radiologists, when given the same image, come to a different conclusion 20% of the time. The same happens to developers when asked about the right way to design a complex software, mobile, or web application. Stick to component design principles and your software will benefit.

DockYard is a digital product agency offering exceptional strategy, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training. With a nationwide staff, we’ve got consultants in key markets across the United States including Seattle, Los Angeles, Denver, Chicago, Austin, New York, and Boston.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box