Ember Best Practices: Functional Programming and the Observer Effect

observer
Lauren Tan

Applications Manager

Lauren Tan

My awesome colleagues have written about patterns and best practices we use at DockYard, covering everything from the Ember Object model and better acceptance tests, to native inputs and closure actions.

Today, I’d like to write about something that is close to all our hearts – observers; and a topic I think about a lot – functional programming (FP). I’d like to take you through a tour of refactoring away from observers in your application and freeing yourself from the shackles of the “observer effect”. Let’s get started!

Side effects wreck projects

In science, the term “observer effect” refers to the changes that are effected on some phenomenon simply by observing it. This is often because of instruments that alter the state of what they observe in some manner.

Sound familiar? Funnily enough, the same effect can probably be found in your Ember application if you use observers. For reasons we’ll cover in this post, observers are an anti-pattern and should be avoided, as it makes it difficult to reason about and to debug your application.

Difficult to reason about? ¯\_(ツ)_/¯

“Difficult to reason about” is a term that is commonly thrown around – but what exactly does it mean? Informally, it means being able to tell what a program will do just by looking at its code. That is, running the program should not lead to unexpected surprises. We strive to make our applications easy to reason about because it leads to more maintainable code that is easier to debug and test.

That is, running the program should not lead to unexpected surprises.

Since JavaScript isn’t a purely functional language, it does not enforce immutability by default. Its mutable data structures can sometimes lead to surprising and unintuitive results (making some JavaScript applications difficult to reason about), so various attempts have been made to introduce immutability to the language.

For example, ClojureScript and Elm are functional languages that compile down to JavaScript. Mori and immutable.js are also excellent libraries that provide immutable data structures you can use in vanilla JavaScript. These tools bring in bits of FP philosophy into JavaScript applications, and have become increasingly popular in front-end development thanks to the introduction of React and the Flux pattern.

The “rise” of FP in front end development

FP is not a new programming paradigm – it’s been around for a while since the introduction of LISP in the late 50s. However, it’s only in recent times that we’ve seen FP languages become more mainstream, most notably with Elixir, which combines the elegance and productivity of Ruby with the performance of the Erlang VM.

The combination of Ember’s conventions (over configuration), Glimmer, and DDAU means that just like Elixir, Ember developers too can find the sweet spot of having both productivity and performance.

Why is FP useful for building front end applications? React taught us that it is surprisingly cheap to diff the changes on the DOM, and to perform a series of updates to sync the DOM with its virtual representation. In other words, React made its render function a pure one (one of the core principles of FP) – you always get the same output (HTML) for a given input (state).

With the Flux pattern, that is taken a step further and the entire application is treated as a pure function – for a given serialized state, we can predictably render the same output each time. This has allowed Flux implementations like Redux the ability to have a “time traveling debugger”, which is only possible when you model your architecture with a FP paradigm.

Object.observe is not the answer

These philosophies have had resounding success, so much so that Object.observe was withdrawn from TC39. All this means is that observing property changes are no longer the best way of building web applications, and Ember in general is moving in that direction with its “Data Down, Actions Up” (DDAU) philosophy.

In 1.13.x, Glimmer landed – it is Ember’s new rendering engine based on the same pure render semantics, and gives us fast, idempotent re-renders. The combination of Ember’s conventions (over configuration), Glimmer, and DDAU means that just like Elixir, Ember developers too can find the sweet spot of having both productivity and performance.

Data Down, Actions Up

Although you could bring in immutable data structures into your application (a topic for a future post!), that alone wouldn’t be enough. We should also strive to make our business logic as pure (free of side effects) as possible. Of course, it wouldn’t be possible to make everything pure, as web applications inherently involve mutation (i.e. updating a user record), but it is beneficial to explicitly know and isolate the functions in your app that do have side effects.

In Ember 2.x, the best way forward is “Data Down, Actions Up”. This means:

  1. One way bindings by default
  2. Explicit closure actions over side effects
  3. Components should not mutate data directly

My colleagues have already written about one way bindings, closure actions and not mutating data in Components, so I will leave you to do further reading in your own time.

Instead, let’s continue the discussion above on how best to refactor away observers in your application, so that we can eliminate or reduce side effects.

Refactoring away observers

Let’s say we have an observer in our application that needs to check a user’s birthday (demo):

// birth-day/component.js
import Ember from 'ember';
import moment from 'moment';

const { 
  Component, 
  computed,
  get, 
  set
} = Ember;

export default Component.extend({
  isBirthday: false,
  birthDate: null,
  
  age: computed('birthDate', {
    get() {
      return moment().diff(moment(get(this, 'birthDate')), 'years');
    }
  }),

  checkBirthday() {
    let today = moment();
    let birthDate = moment(get(this, 'birthDate'));
    let isBirthday = (today.month() === birthDate.month()) && 
      (today.day() === birthDate.day());

    set(this, 'isBirthday', isBirthday);
  },

  init() {
    this._super(...arguments);
    this.addObserver('birthDate', this, this.checkBirthday);
  },

  willDestroy() {
    this.removeObserver('birthDate', this, this.checkBirthday);
  }
});

In the above example, we’re using an observer to set the isBirthday flag if it is the user’s birthday.

{{!birth-day/template.hbs}} 
{{input value=birthDate placeholder="Your birthday"}}

<p>
  You are currently {{age}} years old.
  {{#if isBirthday}}
    Happy birthday!
  {{/if}}
</p>

When you find yourself setting some value (be in on the component, record or somewhere else), you can easily refactor away the observer.

Removing an observer that sets a value on the component

In this scenario, you can simply replace the observer with a CP.

// birth-day/component.js
import Ember from 'ember';
import moment from 'moment';

const { 
  Component, 
  computed,
  get
} = Ember;

export default Component.extend({
  // ...
  isBirthday: computed('birthDate', {
    get() {
      let today = moment();
      let birthDate = moment(get(this, 'birthDate'));

      return (today.month() === birthDate.month()) && 
        (today.day() === birthDate.day());
    }
  })
});

Using component lifecycle hooks

We could also move the input logic out of this component, and use the new component lifecycle hooks to set the isBirthday flag on the component.

// birth-day/component.js
import Ember from 'ember';
import moment from 'moment';

const { 
  Component, 
  computed,
  get
} = Ember;

export default Component.extend({
  birthDate: null,
  today: null,
  isBirthday: false,
  
  didReceiveAttrs() {
    let isBirthday = this.checkBirthday(
      moment(get(this, 'today')), 
      moment(get(this, 'birthDate'))
    );

    set(this, 'isBirthday', isBirthday);
  },

  checkBirthday(today, birthDate)
    return (today.month() === birthDate.month()) && 
      (today.day() === birthDate.day());
  })
});
{{!index/template.hbs}} 
{{one-way-input 
    value=user.birthDate 
    update=(action (mut user.birthDate)) 
    placeholder="Your birthday"
}}
{{happy-birthday today=today birthDate=user.birthDate}}
{{!birth-day/template.hbs}} 
<p>
  You are currently {{age}} years old.
  {{#if isBirthday}}
    Happy birthday!
  {{/if}}
</p>

Both approaches let you remove the observer, but the didReceiveAttrs example is slightly more explicit, and transforms the component into a pure one. By limiting the scope of the component, we can easily isolate the origins of data mutations.

Removing an observer that sets a value outside the component

In this situation, let’s say we want to update the user record’s isBirthday flag instead. We can remove the observer by using one way input actions:

By bringing in the ember-one-way-input addon, we can eliminate the coupling of the component to the user record:

// birth-day/component.js
import Ember from 'ember';
import moment from 'moment';

const { 
  Component, 
  computed,
  get
} = Ember;

export default Component.extend({
  // ...
  actions: {
    checkBirthday(birthDate) {
      birthDate = moment(birthDate);
      let today = moment();
      let isBirthday = (today.month() === birthDate.month()) && 
        (today.day() === birthDate.day());
      
      this.attrs.setIsBirthday(isBirthday);
      this.attrs.setBirthDate(birthDate.toDate());
    }
  }
});
{{!birth-day/template.hbs}} 
{{one-way-input 
    value=user.birthDate 
    update=(action "checkBirthday") 
    placeholder="Your birthday"
}}

<p>
  You are currently {{age}} years old.
  {{#if isBirthday}}
    Happy birthday!
  {{/if}}
</p>

And then in the Controller:

// index/controller.js
import Ember from 'ember';

const { Controller, set } = Ember;

export default Controller.extend({
  actions: {
    setIsBirthday(isBirthday) {
      set(this, 'user.isBirthday', isBirthday);
      user.save();
    },

    setBirthDate(birthDate) {
      set(this, 'user.birthDate', birthDate);
      user.save();
    }
  }
});
{{!index/template.hbs}} 
{{happy-birthday 
    setIsBirthday=(action "setIsBirthday") 
    setBirthdate=(action "setBirthdate")
    isBirthday=user.isBirthday
}}

In the above example, we moved the logic for setting the isBirthday flag out of the happy-birthday component into its Controller. The component no longer needs to directly mutate state (an anti-pattern would be to set user.isBirthday directly in the component), and can instead send a closure action up, leaving the Controller to decide how to handle the actions. Data now flows down to the Component, and any changes to the input sends actions back up.

Who should use observers?

No one.

If observers are so terrible, why are they in Ember? According to Stefan Penner, observers are used by the framework itself as a low level primitive, so that you don’t need to use them yourself. So unless you’re working on a PR for Ember.js, stay away from observers.

I hope you’ve enjoyed this post – next week, stay tuned for Doug’s post on more Ember 2.x best practices!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box