Rationale for Data Down, Actions Up?


#1

Hello Ember community!

I have a question about the rationale behind “Data down, actions up”. All the posts I’ve come across promise vaguely that reliance on two-way bindings “creates difficult-to-debug problems in large applications,” however I’ve never seen a concrete example.

Indeed, the post that comes back first in Google (and is often referenced in other posts) links to an Ember Twiddle that simply could use computed properly (ie, computed('expenses.@each.amount')) instead of passing actions around. Here’s a simpler version of the twiddle without DDAU. Check it out and compare it to the original and explain to me the value of the additional code.

From my point of view, less code is—with a few notable exceptions—invariably easier to read, understand, debug and change. DDAU necessarily results in more code. Furthermore, a large app will have a proliferation of highly nested components, sometimes even recursive components. When you pass actions down, it’s common to rename those actions (sometimes out of necessity to avoid clobbering local vars). I even see actions being renamed in example posts (the one above does it). This can make it extremely confusing to trace back what methods are actually being called, not to mention extremely tedious to pass these actions down to the umpteenth-nested component.

This proliferation of highly nested components and necessary action-passing seems like it would also create a difficult situation in large, complex apps.

But I can’t be sure! I haven’t seen a satisfying explanation or example that really feels simpler or easier to understand or change with DDAU. I really want that “aha!” moment where it becomes obvious that DDAU is better.

Have you seen any posts that do a good job explaining the rationale behind DDAU? Or do you have such an explanation you could share with me?

Thanks!


#2

The “difficult-to-debug” issue stem from there’s no definite answer to what is the exactly value at the binding at any given time.

For example…

Your component has a property default to null. You bind a controller property to that property and the controller has a default value set to 0. The binding is a two way binding. Now you tell me, does the controller win and the component’s property get set to 0? Or does the component win and set the controller property to null?

Ember.Component.extend({
   prop: null
});
Ember.Controller.extend({
   prop: 0
});
{{my-component prop=prop}}
{{prop}} {{! guess what shows up here? }}

#3

Great write up! For most cases, DDAU is very beneficial. I don’t use if for propagating input changes (two-way is simpler here) and such, but for actions and, for example current user data, passing data from a controller can help with a few things:

  1. Testing. When components are dumb, integration tests become that much simpler. If you write all your code with accompanying tests, it is necessary to write production code that is easy to test as well and this is one way to do it.

  2. Refactoring. With actions and some data coming from a single source of truth, when the various child components are refactored, it simply requires a change in the API of the child components rather than moving the source of truth to different levels (to the refactored child components).

  3. Synchronization of data. It is because an app gets sufficiently large that a single source of truth become an ally. If you are adding, modifying and deleting data in ChildComponent when that data belongs to the Controller, tracing down data issues becomes very complex. This is esp true if Child and ParentComponent are modifying the same source of data that belongs to a controller.


#4

First of all, :flushed: YIKES.

Second, I have no idea what would show up there.

Last, how exactly does DDAU solve this problem? If anything, I would expect it to exacerbate this problem because you are necessarily passing around more named values.


#5

A single bi-directional data flow link is not necessarily that bad. It has some ambiguity over which side “wins” under some conditions, and it tends to be sensitive to timing in a way that one-way flow would not be.

But that’s not really the big problem. The big problem is that as you keep adding more links, it’s easy to create loops without realizing it. Once you have a loop, reasoning about the system gets extremely hard. This manifests itself as bizarre bugs, often popping up in places that seem totally unrelated to the changes you just made.

To avoid that complexity, we want the data flow to be a tree, not a graph. To keep it as a tree, you need some rule that will prevent the creation of loops. “Data down” is that rule. As long as data only flows from parents to children, you can’t accidentally make a loop.

“Actions up” is the flip side, because children obviously do need to influence their parents. But they can do so explicitly by asking the parent to change some state rather than implicitly by just reaching out and changing it.


#6

@ef4 @Scott_Newcomer related question: does DDAU basically imply you should never bind a field directly to an ember data model (or change or save an ember data model in a component) because that is an “action” and should be done in the route (considering controllers are going away)?


#7

That is starting to make sense but I still think I need a concrete example for it to really click…


#8

What a GREAT explanation by ef4.

@shull controllers are a great resource for DDAU and other things (query params). I would stick with them when it fits. Let’s say you have a select dropdown that selects a status on a user. You could pass an action from the controller called selectStatus (ACTIONS UP). In the selectStatus action on the controller, you fetch a list of statuses from your API and then on the model user it’s status is changed set(user, 'status', 'Expired') when the user selects a new status. After the controller handles the change, the data is then propagated down the component tree (any sibling components can now pick up that data change as well) - DATA DOWN. Moreover, in integration tests, you can pass a dummy action with a static list of statuses to that component instead of faking out an API request.

Where two-way binding is ok is for a user model with a field name and an input, I wouldn’t personally send up an action to the controller. Just have the {{input value=user.name}} in some child component mutate the name property directly - DATA UP. However, you can imagine sending an action up might be necessary. It all depends on the complexity of the model binding. If a sibling component to the input component is also using that user.name data and subsequently modifying it, then a single source of truth for both components to modify the data is preferred to prevent the loop problem talked about above.


#9

I’m sold on DDAU because I find it much easier to reason about in particular when a property is handed off to multiple components, or down through a component tree.

If you rely on 2 way bindings for setting and lots of components have access to it, then when there’s a bug that relates to that property any component that has access to that property could be the culprit. If you haven’t memorized exactly who is changing that property under what circumstances that can get really messy really fast – not to mention that the same property can have a different key each time it passes to a new component.

Finally, it is not uncommon to want to validate or sanitize values before a property is set, or set other related properties call other functions etc that’s a great place to do it, rather than relying on computed property side effects.

If you don’t use actions, all of that becomes really messy and is likely to become a point of failure.

Sometimes maybe it’s overkill, but if you’re just consistent about it, it’s easy to implement and you don’t ever have to ask yourself “Where the heck is that value coming from??”… and if you do, you only have one place to look.

 //Controller
word:"sandwich",
actions:{
changeWord(newWord){
    if(newWord.length>3){
    this.set('word', newWord);         
    }
}}

//template
{{capitalize-component
  word=word
 doChange=(action 'changeWord')
}}

//capitalize-component .js
actions:{
   doCaptialize(){
   const capWord = this.get('word'). toUpperCase();
   this.get('doChange')(capWord);
   }
}

 //capitalize-component .hbs
 <button {{action 'doCapitalize')>ALL CAPS!</button>

#10

OK so case in point. Suppose you’re building a books app. For the edit route, is this reasonable?

// assume we're using ember-route-action-helper
//
// app/routes/books/edit.js
export default Route.extend({
  model(params) {
    return this.store.find('book', params.book_id);
  },
  actions: {
    saveBook(book) {
      book.save().then(() => {
        this.transitionTo('books.show', book);
      }).catch(() => {
        alert('your edits are invalid! fix them or navigate away to cancel');
      });
    },
    willTransition(transition) {
      let book = get(this, 'controller.model');
      book.rollbackAttributes(); // rolls back unsaved attributes
    },
  },
});

{{!-- app/templates/pages/edit.hbs --}}
<form onsubmit={{route-action 'saveBook' model}}>
  <label>
    Title
    {{input type='text' value=model.title}}
    {{errors msgs=model.title.errors}}
  </label>
  <label>
    Author
    {{input type='text' value=model.author}}
    {{errors msgs=model.author.errors}}
  </label>
  <label>
    Publisher
    {{input type='text' value=model.publisher}}
    {{errors msgs=model.publisher.errors}}
  </label>
  <input type='submit' value='Save' />
</form>

{{!-- app/templates/components/errors.hbs --}}
{{#if msgs}}
  <small class='error'>{{msgs.firstObject}}</small>
{{/if}}

Notes:

  • assuming your server runs validations and responds with properly-formatted errors, ember-data automatically adds errors to the book and the form will render them
  • if you start editing a book and navigate away without saving (or if there were validation errors and you give up), the route’s willTransition hook rolls back unsaved attributes

Questions:

  • would you use willTransition in this way? if not, why?
  • would you extract this form into a component? if so, would you pass it a book or individual book attributes (eg {{book-form title=book.title author=book.author publisher=book.publisher}})?

Thanks for reading :smile:


#11

One last note for anyone who’s stumbling on this thread. My colleague sent me this video recently and it’s awesome. Best explanation of rationale for DDAU I’ve seen so far, as well as some explanation about when to use it and when it’s not really appropriate.

Please note the video is behind a paywall, but it’s worth IMO.

The tl;dr is basically that when you are building reusable components, you really need to adhere to DDAU. But when you’re building what he calls “smart components” that are specific to a single use case in your app, DDAU adds unnecessary burden without providing much value. So if you’re building ember addons, you probably are building components that are all DDAU. If you’re building a {{blog-post-form}} component in an app, it is going to be smarter because it can make some assumptions about how you’re using it (ie, to create or edit a blog post).