Screen_Shot_2014-05-30_at_1.37.33_PM

Salsify Engineering Blog

Organizing Data Flow with Ember

Posted by Salsify

Apr 11, 2017 1:38:23 PM

One of the most important core principles of developing with Ember is "data down, actions up." You might see this concept abbreviated as DDAU in various Ember communities. The main premise of DDAU is that data should flow down through your component hierarchy (passed through and potentially modified by various components), while changes to said data should be propagated back up through that hierarchy via actions.

In the past, various frontend frameworks touted their ability to support two-way data-binding or the idea of having changes to data flow both up and down through the view layer. That pattern was soon found to be hard to reason about and, in many cases, made the application state difficult to determine both conceptually and programmatically. DDAU is Ember's solution, and it makes complex state much easier to coordinate and disseminate through your application.

This pattern is also known as "unidirectional data flow" and discourages mutating state in-place in your components. As I expand on below, implementing components as either "smart" or "dumb" will lead to clean state management in your application that will be easier to extend and understand.

Ember 0410 blog graphic.png

Graphic by @teropa

Organizing DDAU

The easiest way to organize your components in such a way as to enable clean DDAU architecture is to divide them into two different types: stateful containers and stateless presentational components. You can think of these two categories as “smart” and “dumb.” An Ember controller is a great example of a stateful container: it is a singleton that stores values (state) and provides ways to update those values through actions. It is the single source of truth for the corresponding template. Another viable pattern (which I follow in my example code) is to create "container" components as the top-level construct for your view.  These are technically no different than other components aside from the fact that they will contain mutating state.

 

{{!-- main/template.hbs --}}

{{main-container title="Play some sounds"}}

Incidentally, organizing your components this way might also help migrating to routable components once they land. Subsequent components under the container should strive to be presentational only, but nesting multiple levels of containers in your view tree is also acceptable.

Smart vs Dumb

So, what does a "smart" component look like? Pretty much the same as any other component. The key difference is self-imposed: smart components should be the only place that you mutate component data members in place. Let's look at an example from a simple Web Audio app I've created. The app itself is small, just two routes (only one is consequential) with a handful of components. You can play with the app live here, and I've linked to the GitHub repository for the app at the end of the post. To begin, we'll take a look at some portions of the main container component. The majority of the properties are related to manipulating the state of the oscillator that is used to generate sound.

 

/* main-container/component.js */

export default Component.extend({
  playing: false,
  oscillatorTypes: A(['sine', 'square', 'sawtooth', 'triangle']),
  oscillatorFrequency: 500,

  oscillatorType: computed(function() { return this.get('oscillatorTypes.firstObject'); }),
  audioContext: computed(function() { return new AudioContext(); }),
  oscillator: computed(function() { return this.generateOscillator(); }),

  generateOscillator() { /* generates a new oscillator object using the AudioContext */ },

  startOscillator() {
    this.set('oscillator', this.generateOscillator());
    this.get('oscillator').start();
    this.set('playing', true);
  },

  stopOscillator() {
    this.get('oscillator').stop();
    this.set('playing', false);
  },

  updateOscillator() {
    const oscillator = this.get('oscillator');
    oscillator.frequency.value = this.get('oscillatorFrequency');
    oscillator.type = this.get('oscillatorType');
  },

  actions: { /* see below */ },
});

 

The actions below are strictly for manipulating the container's state and will be passed to child components to be invoked on user input. Passing actions this way allows the components to be naive about what operations they are performing, and thus allows for greater component reuse throughout your application.

 

/* main-container/component.js */
actions: {
  changeFrequency(newFreq) {
    this.set('oscillatorFrequency', newFreq);
    this.updateOscillator();
  },

  changeType(newType) {
    if (this.get('oscillatorTypes').includes(newType)) {
      this.set('oscillatorType', newType);
    }

    this.updateOscillator();
  },

  play() { this.startOscillator(); },
  stop() { this.stopOscillator(); },
},

 

In the template you can see how multiple components are passed closure actions and state from the container. Ideally, none of these components will mutate those values and will instead send new values "up" to the container via the actions passed.

 

{{!-- main-container/template.hbs --}}
<h2>{{title}}</h2>
{{selector-control options=oscillatorTypes selected=oscillatorType onChange=(action 'changeType')}}
{{stepper-control min=0 max=22000 smallStep=100 largeStep=1000 value=oscillatorFrequency onChange=(action 'changeFrequency')}}
{{play-button playing=playing onClick=(if playing (action 'stop') (action 'play'))}}

 

Note that all of the business logic is implemented here. In a larger app you can imagine that any code dealing with the Web Audio API would be moved into a service and could be shared amongst many different containers.

The main advantage to grouping state into a single component is that it allows you to coordinate state changes from a single location. This is often referred to having a “single source of truth.” The state is stored in one place only and is piped to components that need to access it via properties. Without this, parts of state are often passed to multiple components and modified in multiple places. This quickly becomes untenable. While calling functions to change data can seem tedious when multiple levels of component hierarchy are involved, it's nearly always preferable to the alternative.

Imagine that you are starting to work on a large application that you don't have any experience with, and many components in this project are a mixture of smart and dumb. If there is a deep component tree, and no clear indication where state manipulation logic resides, it can be an extremely frustrating experience tracking down the reason your state went from A to B. Did some component quietly mutate a value? Is there a bug, or is that the intended behavior? How do all these state changes in disparate components work together?

By sending an action up to a singular repository for your logical domain's state you get both an explicit notification that the state should change and also knowledge of how multiple changes will fit together to form a complete state picture.

Presentational Components

The rest of the app is comprised entirely of dumb components - these are components that only pass data through attributes to other components, and expose functions that can be invoked to signal changes to data that was passed to them. A really simple example is the "play/stop" button:

 

{{!-- play-button/template.hbs --}}

<button onclick={{onClick}}>
  {{#if playing}}
    Stop
  {{else}}
    Play
  {{/if}}
</button>

 

In fact, this component is simple enough to not require a component.js file. This is similar to React's concept of purely functional components. The component only needs two attributes passed to it: playing and onClick. No state is saved, and "changes" are indicated by calling onClick when the button is clicked. The text on the button is controlled by the other value passed in. Notice that clicking the button does not necessarily change the text in the button; this is left up to the consumer of the component.

You can imagine a world where maybe the text should only change once some asynchronous operation completes. In that case, you'd want to wait to change the value of playing on the component, and if that logic was contained in the component itself it would seriously hinder reusability. This is the power of dumb components.

Testing

Another huge benefit of presentational components is that they are much easier to test. When structured right, presentational components don't need any complex simulated user interactions to get the component to the state you need to test.

For instance, let's imagine we have two components: one is the play-button from the example project, and the other is the stateful play/stop button described above. For the dumb play-button we only need to vary the value of the playing attribute passed in to validate the multiple possible states. In addition you can pass a mocked function into the onClick attribute and ensure that it is called when the button is clicked.

In a stateful version of this component, you might need to simulate a user clicking the button, which will need to interact with the DOM. Additionally, you'll need some way to verify that the click actually worked and only then can you detect if the button text is correct - you can probably see how this could explode in complexity for a real-life component with lots of state. Small, stateless components composed together inherently create easier to test applications.

Recap

Hopefully at this point you've started to understand the importance of giving thoughtful consideration of how you arrange state in your application's component tree. Smart and dumb components give UI engineers a simple-yet-powerful paradigm to follow to prevent confusing, tangled state modifications. If you have any questions about strategies to implement this pattern, or just about Ember in general, feel free to shoot me a DM in the Ember Community Slack group @devers.

Links and Sources

comments powered by Disqus

Recent Posts