The Risks You Take When You Use Ember.computed.oneWay

5 minute read

(Note: this post originally appeared on my company AlphaSight’s blog. You can view that version here.)

The risks you take when you use Ember.computed.oneWay

Ember, the framework for building Ambitious Applications(tm), provides developers a number of tools to build apps quickly. One of the most used tools in this repertoire is Ember.computed, a suite of functions that transform data so that it can be used and manipulated across your application.

Some of the functions provided by Ember.computed are pretty straightforward. Ember.computed.empty takes a stringified key for an array, and returns true or false depending on whether or not that dependent property contains any elements. Ember.computed.gte takes a stringified key that represents a number and an actual number, and returns true or false based on whether or not that dependent property is greater than or equal to that second number.

While the more mathematical computed functions are simple to understand, there are a few more tools provided by computed which need clarification.

alias, readOnly, oneWay

There are several functions that provide a way of renaming or reassigning other values for use in an Ember Object. Don’t feel like writing get(this, 'todo.user.name') over and over, and wish there was a faster way? Ember’s got ya covered.

alias

Ember.computed.alias provides a bi-directional data flow property in your Ember object that allows you to write a shortcut to another property. Referencing the above example, if you kept referencing todo.user.name repeatedly in your Object, you could write a computed.alias to make get and set actions a few characters shorter.

import Ember from 'ember';

const { computed } = Ember;

export default Ember.Component.exend({
	todo: null,
	userName: computed.alias('todo.user.name'),
	//...
});

With a set up like this, you can reference todo.user.name elsewhere in your component (and its template!) without reaching through todo and user every single time.

With computed.alias, get and set actions onto the aliased property behave exactly as if you were referencing the original property. This means that if you’re using a component to set the userName, that would be reflected on that User model in the store.

readOnly

Ember.computed.readOnly is almost the same thing, with one very important difference. The set up for a readOnly property is similar:

import Ember from 'ember';

const { computed } = Ember;

export default Ember.Component.exend({
	todo: null,
	userName: computed.readOnly('todo.user.name')
	//...
});

In this setup, calling get to userName is exactly what you expect. However, trying to call set on this property actually raises an error! You would see this in your devTools if you called set on a readOnly property:

Cannot set read-only property 'userName' on object: <twiddle@component:my-component::ember323>

The readOnly function provides us a healthy reminder about what we should and should not be setting in our application.

oneWay

The Ember.computed.oneWay function is the trickiest one to understand, and also one that is potentially dangerous! Let’s explore why.

The setup is essentially the same…

import Ember from 'ember';

const { computed } = Ember;

export default Ember.Component.exend({
	todo: null,
	userName: computed.readOnly('todo.user.name')
	//...
});

With oneWay, properties act as an alias to their dependent property until they are set. This means that the two properties, once representing the same data, diverge and no longer represent the same value. Suddenly, the data flow has been broken. The original property and oneWay property no longer have a connection, as it’s understood that the oneWay property has been updated in a way that isn’t expected to affect the original object.

What Does This Mean For My Application?

The computed.oneWay property can be great for things like forms, where you really don’t want a new input to immediately persist a change to a model’s field in the Ember Store. However, certain code practices can cause oneWay to unexpectedly create incorrect displays to the user. Let’s explore how.

Consider you have a collection of Todos and are using computed properties to display the data about those todos to the user. Consider the following template:

//templates/application.hbs

<div class="todos">
  <div class="selected-todo">
    {{#if selectedTodo}}
      {{todo-display todo=selectedTodo}}
    {{else}}
      <div>Please View A Todo.</div>
    {{/if}}
  </div>
  <br>
  <div>
    {{#each todos as |todo|}}
      <button {{action (mut selectedTodo) todo}}>
      	View {{todo.title}}
      </button>
    {{/each}}
  </div>
</div>

and in the todo-display component/template:

import Ember from 'ember';

const { computed, set } = Ember;

export default Ember.Component.extend({
  todo: null,
  title: computed.oneWay('todo.title'),
  body: computed.oneWay('todo.body'),

  actions: {
    updateBody(){
      set(this, 'body', 'jk, do this thing. haha! youre stuck doing this thing instead');
    }
  }
});

<div class="todo">
  <div class="todo__content">
    <h3>{{title}}</h3>
    <div>{{body}}</div>
  </div>
  <button {{action "updateBody"}}> Update body </button>
</div>

This example’s application/template.hbs provides a few buttons to set a selectedTodo property, and pass that into the todo-display component.

In the todo-display component, we are using computed.oneWay properties to display the todo’s title and body. We also have an action that overwrites the component’s body property.

The entirety of this example is below in an Ember Twiddle.

In this scenario, toggling between Todos at first updates all of the data in the todo-display component correctly. However, after the first time the updateBody action is called, the data flow from todo.body to body on the component is broken. Thus, every subsequent toggle to a different Todo does not update the body property in the component.

This can be dangerous because your application suddenly has mixed state. Now, your users are able to view Todo B with a different body that was actioned when viewing Todo A. If you were performing an action based on the todo-display’s body and title properties, your action may not have the state it was intended to!

This behavior can be useful in other cases. For example,Ember.computed.oneWay can be useful when building a form for a single model. As long as the component is properly destroyed, it would not have a chance to affect another model. If we’re using the same component for multiple models, that’s where the problem occurs.

How Can I prevent this?

One of the problems with this setup is the fact that different todos are being passed into the same todo-display component, and the state of that component is not being reset correctly whenever data changes. One fix to this could be:

//components/todo-display.js
import Ember from 'ember';

const { computed, get } = Ember;

export default Ember.Component.extend({
  todo: null,
  title: computed.readOnly('todo.title'),
  body: null,

  didReceiveAttrs(){
  	let body = get(this, 'todo.body');
  	set(this, 'body', body);
  }
  //...
});

By hooking into the lifecycle of a component, we ensure that we are setting up our component the same way every time a new todo is passed in. Now, our todo-display component is more robust and does not display our todos incorrectly!

These are but a few of the powerful tools provided by Ember.computed. Now that you’ve read this post and understood the differences between alias, readOnly, and oneWay, go forth and create some side-effect-free components! Cheers ;)

Updated: