Data down, actions up
Ember 2.0 is embracing some new patterns to ensure your apps stay clear and maintainable as they grow in complexity. One of these patterns is "data down, actions up." But what does that mean?
Here's a good way to think about it. Let's say we're making something like Trello, and our index route looks like this:
//routes/index.js
export default Ember.Route.extend({
model: function () {
return this.store.find("card");
},
});
We want to display the cards in our template:
<!-- templates/index.hbs -->
<div class='board'>
</div>
Note: Soon, this will simply be
each card in cards
. No more proxies.
We've made a CardSummaryComponent
to wrap up the display logic for each card we're showing. Let's say this summary shows the title of the card, and has an "X" button in the top-right which lets our users delete the card. We can imagine writing something like this:
<!-- templates/components/card-summary.hbs -->
<div class='card'>
<h2></h2>
<span class='fa fa-remove'> X </span>
</div>
and the component code
//components/card-summary.js
export default Ember.Component.extend({
actions: {
removeCard: function () {
return this.get("card").destroyRecord();
},
},
});
Now, the problem here is that the CardSummaryComponent
is deleting the data, but it's really the IndexController
that owns the data. Its the one that passed the card
into the CardSummaryComponent
, and now that component has gone and deleted its data right out from under its feet. This is sort of thing that becomes really difficult to trace in larger, more complex applications.
Note that in Ember 2.0, we wouldn't have an IndexController
but rather a routable component. We don't know exactly how the API will shake out, but somehow we'd end up with a component that has an attrs.cards
property from the server, which would be the array of cards. We'd probably then make a separate CardSummaryListComponent
and do something like {{card-summary-list cards=cards}}
.
In any case, back to the example. This clearly violates "data down", because there was a data change in a child component (CardSummary
) which affected parent components (in this case the each
block, in the alternative case the CardSummaryList
component).
To revise this, first, pretend that you live in a world without two-way bindings. That is, when we render {{card-summary card=card}}
, what's actually happening is the card
from the parent scope is being "copied", and passed into component, which now has the data in its own isolated scope. Any change to card
in the parent scope will re-push that data into the card-summary
component, which is the "data down" part. But, if the card-summary
component mutates its own card
data, none of its parent components will be the wiser. That's one-way bindings.
But of course, we want those interactions in our child components to actually affect our real data. So, we use actions to send those messages back up our hierarchy. This is "actions up", and the idea is that our card-summary
component is simply "notifying" its parent components that a certain action has taken place - "deleteCard", for example. It's up to the parent components to decide how they want to handle the actions their children are emitting.
In React, those parent components do this by explicitly passing their action handlers into their children. Something like this:
//components/card-summary-list.js
// Here's the JSX template for a <CardSummaryList>
cards.map(function (card) {
return <CardSummary card={card} onCardDelete={this.handleCardDelete} />;
});
Here, CardSummaryList
is passing a handler down to its child. The CardSummary
component can then simply invoke this handler in response to whatever event it chooses. The child determines the action, but the parent determines how the action is handled.
In Ember, we're used to doing something more along the lines of
<!-- templates/index.hbs -->
<div class='board'>
</div>
and then writing the deleteCard
handler within the parent. This is changing, for a few reasons (read the Improving Actions section of the 2.0 RFC for more info). In 2.0 we'll be doing things similar to how React does it, by passing actions directly in:
<!-- templates/index.hbs -->
<div class='board'>
<card-summary card=card deleteCard=>
</div>
The CardSummary
component will have access to that action via attrs['deleteCard']
, and will be able to invoke it just like in React. This way, the child component is effectively delegating the event handler to the parent, without knowing anything about what that handler does. As an added bonus, the child can continue to pass the parent's handler further down to its own children - without the need for any sendAction
boilerplate in the actual JS code.
This is a simple example of how "data down, actions up" should be influencing the architecture of your Ember apps. If you haven't already, I highly recommend spending several hours with React. It's a great way to learn about component-based architecture and immutability, two concepts that will be making a strong showing in Ember 2.0.