Ember Data: Worryless Model Defaults

By: Romina Vargas
worry

When working with Ember Data models, it’s common to want to set default values for certain attributes. Setting a default value on a model is super easy and you’ve probably done it countless times:

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  isHungry: attr('boolean', { defaultValue: true })
});

The above syntax works flawlessly for Boolean, String, and Number types. But what if you want to set defaults on an Object or an Array type?

Before answering that question, you should note that Ember Data does not have out of the box support for Object and Array types. Well, kinda. If you don’t specify a type as the first argument to DS.attr, it just means the value for that attribute will be untouched rather than coerced to the matching JavaScript type. You can easily add a transform for a custom type. Your transform should provide serialize and deserialize methods for proper processing.

Now to answer the above question, your first instinct might be to do the following:

// app/models/person.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  favoriteThings: attr('object', { defaultValue: {} })
});

Here, every new record created with the Person model would be expected to have a favoriteThings value of {}, rather than undefined. Which is correct, but only until you begin setting content on that object. An Ember Data model extends from Ember.Object, meaning that arrays and objects will be shared among all instances of that model. If you’re not too familiar with that concept, check out this past Ember Best Practices blog post from Estelle!

The result from setting a model attribute default to an empty object:

let foo = this.store.createRecord('person');
get(foo, 'favoriteThings'); // => {}
set(foo, 'favoriteThings.food', 'pozole');
get(foo, 'favoriteThings'); // => { food: 'pozole' }

let bar = this.store.createRecord('person');
get(bar, 'favoriteThings'); // => { food: 'pozole' }

Trolling at its finest. Bar doesn’t even know what “pozole” is. (By the way, if you’ve never had Mexican pozole, you’re missing out!)

This quirky functionality isn’t particular to Ember, however. It all stems from JavaScript itself; this also happens with POJOs:

var foo = { name: 'foo' };
var bar = foo;

bar.name = 'bar';

console.log(bar); // { name: 'bar' }
console.log(foo); // { name: 'bar' }

Lucky for us, Ember has made it easier to avoid this pitfall by deprecating the use of a complex object as a default value for a model property. If you try to set an attribute with a default value of type "object", you’ll see a warning message: Non primitive defaultValues are deprecated because they are shared between all instances. If you would like to use a complex object as a default value please provide a function that returns the complex object. This is your cue to undo.

A better, and often forgotten option

Don’t fret, because this issue is just a simple fix away. defaultValue also accepts a function. Hooray! Let’s modify our code to work as we would expect.

// app/models/person.js

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  favoriteThings: attr('object', { defaultValue: () => {} })
});

That’s it! This will ensure that every favoriteThings attribute contains its own object instance. Having the ability to pass in a function to defaultValue can also prove helpful if you would like to set custom defaults based on computed properties, as well as other attributes of the same model.

Hope this served as a sweet reminder, or as something new that you can leverage in your projects from now on.