Discoveries from this exploration have been captured in an experimental add-on, ember-seamless-immutable.

Immutability makes reasoning about data flow easier on the brains. By disallowing changes to data, you can avoid the problem of side-effects, allow for easier versioning of state (handy for undo/redo), and it nicely complements a “data down, actions up” (DDAU) approach. You can read more about “Immutable Data Structures in JavaScript” from James Long’s excellent write-up.

Ember.js has embraced the DDAU approach to data flow since it’s 2.x release. You can begin to reap benefits of immutability right now.

Avoid two-way bindings by leveraging mut and readonly helpers (or using glimmer components)

Object.freeze() your models for shallow objects

When working with arrays, use functions that return new arrays like map or filter, rather than mutates

Immutable data structures

I mentioned that Object.freeze() should be used for shallow objects. This is because values that are objects can still be modified despite being “frozen”. To attain the peace of mind that comes with knowing for certain that child components aren’t going to inappropriately muck with your model, you’ll need to take your immutability up a notch and leverage immutable data structures.

One popular implementation comes from Facebook in the form of Immutable.js. Loads of great features, but not-so-great interoperability. Seamless-immutable is a light-weight solution that uses plain JavaScript objects and offers excellent interoperability. To me this felt like the quickest way to immutable data structures in my Ember app.

There were a couple gotchas to work around using seamless-immutable data structures in Ember.

First off, not surprisingly, Ember’s nifty Array prototype extension is incompatible with seamless-immutable. And so it must be disabled.

Next, when an object property rendered in a template, Ember defines a meta hash on objects to help manage things like bindings, mixins, etc. Of course, defining any property on an immutable object is a problem. To workaround this issue, a no-op function is defined on all objects made immutable to circumvent Ember defining meta properties on them

An experimental add-on

I packaged these workarounds along with some other niceties into an add-on. How does it work? It goes a little something like this:

// template.hbs {{contact-detail model=(immutable contact)}} // contact-detail/component.js export default Ember.Component.extend({ //... actions: {

naughtyEditCity(newCity) {

// you bet your bippy this will throw an error

this.get(‘contact’).address.city = newCity;

},

editCity(newCity) {

this.getAttr(‘editCity’)(newCity);

}

}

}); // route.js export default Ember.Route.extend({ // ...



actions: {

editCity(newCity) {

const contact = this.currentModel.contact.asMutable({

deep: true

});

contact.address.city = newCity;

}

}

});

Will this work?

I plan to use this add-on with a couple upcoming Ember.js projects to test its mileage. Any and all feedback is welcome. In the future, I’d love to see Ember.js and Ember Data more fully embrace immutability.