Readers' Questions: "What is meant by the term 'Data down, actions up'?"

Hello once again to Readers’ Questions, presented by the folks at The Ember Times :newspaper::slightly_smiling_face:

Today I’ll be answering a question by Daniel who is interested to know more about a particular Ember concept:

What is meant by the term “Data down, actions up”? Is it an important concept to learn?

“Data Down, Actions Up” has become one of the most mentioned patterns in the Ember ecosystem. But what does it mean?

What is meant by “Data Down, Actions Up”?

The term “Data Down, Actions Up” (DDAU) describes a common pattern found in Ember applications which emphasizes unidirectional data flow. The term describes the practice of passing data, e.g. in form of an Ember Data record or a plain JSON object, from one part of the app which “owns” the data down to other application layers. In Ember apps the data owner will oftentimes be a Controller or a Route, but it could also be a data loading Component; on the other hand the recipient of passed down data is oftentimes another sub route or Component.

The data recipient might have access to said to data to, for example, present it in the UI, but in DDAU it will never modify that data directly. Instead, with DDAU a component will only have access to one or several actions: callbacks which can be triggered through the component to modify data indirectly. These actions are passed down from the data owner and they will be invoked on the very same parent layer they originate from.

This - at first - sounds a bit cumbersome. Why would you not want to modify the data directly where it is used and instead decide to pass down both data and callbacks for modifying that data to modify said data to another or potentially several layers of routes and components? What are the benefits of DDAU?

The advantages of DDAU

The benefits of DDAU can be broken down into the following three points that help us to develop Ember apps more easily:

DDAU helps with a clearer separation of concerns

Once we start to handle the mutation of data in one specific spot (in Ember we’d most often do so at the Route or Controller layer) and only pass down data from this layer to components or a tree of components for presentation - it becomes clear to us where to look for the setup of data in our app right off the bat. Similar to the naming conventions we are used to from Ember’s application model, this convention helps us to save time ruminating on where our data might have to be setup. In a similar fashion that a developer with some previous experience with Ember is able to find a particular model of name foo in the respective app/models/foo.js path instantly, a developer who practices DDAU in their app can be sure to find the places in which data is being mutated in the blink of an eye, too. Therefore DDAU represents a benevolent convention to help us find where data is originating from in our codebase.

DDAU helps with component reusability

Making our Ember components as reusable as possible helps us to avoid developing similar features repeatedly. DDAU enforces component reusability by removing responsibilities from the UI which may change depending on the context the component is used in.

DDAU helps to avoid complex data loops

Cycles in the application’s data flow can be hard to reason about and therefore become prone to unexpected bugs. Once loops in our app’s data flow emerge, the application state might change in a time-sensitive manner and it becomes more unclear to which extent one function that mutates data “wins” over another.

To avoid having to manage cyclic data flows in Ember apps, DDAU emphasizes a clear, unidirectional data flow.

An Example of DDAU

Let’s take a look at a specific example. Let’s imagine we’re building an application that has a /profile route. On the profile page user can change the language in which they view the app to be e.g. Portuguese instead of English.

In the /profile route we can pass down the user data to the related context:

// app/routes/profile.js
import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.get('store').findRecord('user', 1);
  },
});
// app/controllers/profile.js
import Controller from '@ember/controller';

export default Controller.extend({
  languages: ['en', 'zh', 'hi', 'es', 'pt', 'fr', 'it', 'de'],
});

As we build along this feature we might want to create a language-picker component which offers a UI for changing to the user’s language settings.

{{! app/templates/profile.hbs }}
{{language-picker user=model languages=languages}}

The language picker displays the user’s current language choice and a list of different locales a user is able to choose from:

// app/templates/components/language-picker.hbs
<p>Current Language: {{user.selectedLanguage}}
<select>
  {{#each languages as |language|}}
    <option value={{language}}>{{language}}</option>
  {{/each}}
</select></p>

And once a language is selected, it will 1. set the correct locale for the web app and 2. update the user settings to reflect that choice. We could handle all of this in an action - let’s call it updateLanguage inside of the language-picker component itself:

import Component from '@ember/component';

export default Component.extend({
  languages: null,
  user: null,
  actions: {
    updateLanguage(ev) {
      let language = ev.target.value;
      this.setupTranslations(language);
      this.set('user.selectedLanguage', language);
    },
  },
});

And subsequently attach this action to the language-picker:

// app/templates/components/language-picker.hbs
<p>Current Language: {{user.selectedLanguage}}
<select  onchange={{action "updateLanguage"}}>
  {{#each languages as |language|}}
    <option value={{language}}>{{language}}</option>
  {{/each}}
</select></p>

So far, so good. Yet, we can already see one problem with this approach: This component is only built for one specific use case - the setting of the user locale - and cannot be reused in other parts of the app easily. If we wanted to make this component reusable, we’d need to make sure that any changes to the application state when changing the language are not handled inside of the component itself, but in the external context instead.

We can do so by extracting this data mutating functionality and passing it down from the data owner to the component as a callback. This action can be triggered through the component once the value of the select element changes. It will then be invoked on the parent (the profile controller in this example):

// app/controllers/profile.js
import Controller from '@ember/controller';

export default Controller.extend({
  languages: ['en', 'zh', 'hi', 'es', 'pt', 'fr', 'it', 'de'],
  actions: {
    updateLanguage(ev) {
      let language = ev.target.value;
      this.setupTranslations(language);
      this.set('user.selectedLanguage', language);
    },
  },
});
{{! app/templates/profile.hbs }}
{{language-picker
  languages=languages
  user=model
  onLanguageChange=(action "updateLanguage")
}}
{{! app/templates/components/language-picker.hbs }}
<p>Current Language: {{user.selectedLanguage}}
<select  onchange={{action onLanguageChange}}>
  {{#each languages as |language|}}
    <option value={{language}}>{{language}}</option>
  {{/each}}
</select></p>

Which allows us to entirely remove the action from our language-picker component configuration, as the functionality now lives on the controller:

// app/components/language-picker.js
import Component from '@ember/component';

export default Component.extend({
  languages: null,
  user: null,
});

The language-picker component passed on the responsibility of data mutation to the data owner - in this example the profile controller - making the component reusable in other contexts with different functionalities. The component now only presents the data which has been passed down to it and provides a handle for user interaction to trigger data and app state mutation externally.

This way it is not possible for a circular data flow to emerge either. This allows data to be mutated with a predictable outcome.

It’s also important to note, that any data that is passed in an Ember app cannot be guaranteed to be immutable. Therefore DDAU is a useful pattern to make data flow predictable in Ember apps today. If you want to learn more about data immutability and how it could play out in future Ember apps, be also sure to check out @dgeb’s answer to one of our previous Readers’ Questions here.

Further Reading

The Data Down, Actions Up Paradigm is inspired by well-tested patterns practiced in other JavaScript communities. For example, you can learn more about the benefits of DDAU in this blog post about the separation of concerns through Container & Presentational Components in React.

If you want to learn more about the “Data Down, Actions Up” pattern in Ember specifically, I can highly recommend to give this discussion thread on the Ember forum a read and to watch this excellent (and free!) video on how to create reusable components featuring DDAU over at our friends’ at EmberMap.


This answer was published in Issue #60 of The Ember Times. Don’t forget to subscribe to the newsletter to get new Readers’ Questions and other development news right in your inbox. You can also submit your own questions! You will find a link at the bottom of the newsletter.

See you in the next issue! :sparkling_heart:

19 Likes

Jessica, thank you for such a detailed explanation. One quesiton: so what will be the final content of language-picker.js file after we moved the data manipulation to the profile.js controller ? Thank you.

2 Likes

The component would finally be defined as follows:

// app/components/language-picker.js
import Component from '@ember/component';

export default Component.extend({
  languages: null,
  user: null,
});

In this case you could even omit the .js file entirely, to make it a template-only component. I also updated the answer above to reflect this final change. Thank you for your feedback @belgoros!

2 Likes

Jessica, in case of apps with multiple components on the page, won’t the controller get bloated this way? And what actions would be good if there are kept in the component? I assume the actions that don’t deal with data.