Ember - Best Practices: Avoid Leaking State Inside the Factory

Original author: Estelle DeBlois
  • Transfer

At DockYard, we spend a lot of time on Ember, from building web applications, creating and maintaining add-ons, to contributing to the Ember ecosystem. We hope to share some of the experience we have gained through a series of posts that will focus on Ember best practices, patterns, antipatterns, and common mistakes. This is the first post in this series, so let's start by returning to the basics Ember.Object.


Ember.Objectthis is one of the first things we learn as Ember developers, and no wonder. Almost every object we work with in Ember, whether it is a route (Route), a component (Component), a model (Model), or a service (Service), is inherited from Ember.Object . But from time to time, I see how it is used incorrectly:


export default Ember.Component.extend({
  items: [],
  actions: {
    addItem(item) {
      this.get('items').pushObject(item);
    }
  }
});

For those who have come across this before, the problem is obvious.


Ember.Object


If you look at the API and deselect all Inherited , Protected , and Private options, you will see that it Ember.Objectdoes not have its own methods and properties. Source code cannot be shorter. This is a literal extension of Ember CoreObject, with an admixture of Observable:


var EmberObject = CoreObject.extend(Observable);

CoreObjectprovides a clean interface for defining factories or classes . This is essentially an abstraction around how you usually create a constructor function, defining methods and properties on a prototype, and then creating new objects with a call new SomeConstructor(). For the opportunity to call superclass methods, using this._super(), or to combine a set of properties into a class through impurities , you should thank CoreObject. All methods that often have to be used with Ember objects, such as init, create, extend, or reopen, are determined in the same place.


Observablethis is an admixture (Mixin), which allows you to observe changes in the properties of the object, as well as at the time of the call getand set.


When developing Ember applications, you never have to use it CoreObject. Instead, you inherit Ember.Object. After all, Ember has the most important response to changes , so you need methods with Observableto detect changes in property values.


New Class Announcement


You can define a new type of observable object by expanding Ember.Object:


const Post = Ember.Object.extend({
  title: 'Untitled',
  author: 'Anonymous',
  header: computed('title', 'author', function() {
    const title = this.get('title');
    const author = this.get('author');
    return `"${title}" by ${author}`;
  })
});

New type objects Postcan now be created by calling Post.create(). For each record, the properties and methods declared in the class will be inherited Post:


const post = Post.create();
post.get('title'); // => 'Untitled'
post.get('author'); // => 'Anonymous'
post.get('header'); // => 'Untitled by Anonymous'
post instanceof Post; // => true

You can change the values ​​of the properties and give the post the name and name of the author. These values ​​will be set on the instance, and not on the class, so they will not affect the posts that will be created.


post.set('title', 'Heads? Or Tails?');
post.set('author', 'R & R Lutece');
post.get('header'); // => '"Heads? Or Tails?" by R & R Lutece'
const anotherPost = Post.create();
anotherPost.get('title'); // => 'Untitled'
anotherPost.get('author'); // => 'Anonymous'
anotherPost.get('header'); // => 'Untitled by Anonymous'

Since updating properties in this way does not affect other instances, it is easy to think that all operations performed in the example are safe. But let us dwell on this a little more.


State leak inside class


A post can have an additional list of tags, so that we can create a property with a name tagsand by default it is an empty array. New tags can be added by calling the method addTag().


const Post = Ember.Object.extend({
  tags: [],
  addTag(tag) {
    this.get('tags').pushObject(tag);
  }
});
const post = Post.create();
post.get('tags'); // => []
post.addTag('constants');
post.addTag('variables');
post.get('tags'); // => ['constants', 'variables']

It looks like it works! But let's check what happens after creating the second post:


const anotherPost = Post.create();
anotherPost.get('tags'); // => ['constants', 'variables']

Even if the goal was to create a new post with empty tags (assumed by default), the post was created with tags from the previous post. Since the new value for the property was tagsnot set, it simply mutated the main array. So we effectively threw the state into a class Post, which is then used on all instances.


post.get('tags'); // => ['constants', 'variables']
anotherPost.get('tags'); // => ['constants', 'variables']
anotherPost.addTag('infinity'); // => ['constants', 'variables', 'infinity']
post.get('tags'); // => ['constants', 'variables', 'infinity']

This is not the only scenario in which you can confuse the state of an instance and the state of a class, but it is, of course, one that is more common. In the following example, you can set the default value createdDatefor the current date and time by passing new Date(). But it is new Date()computed once when the class is defined. Therefore, regardless of when you create new instances of this class, they will all have the same value createdDate:


const Post = Ember.Object.extend({
  createdDate: new Date()
});
const postA = Post.create();
postA.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)
// Sometime in the future...
const postB = Post.create();
postB.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)

How to keep the situation under control?


In order to avoid sharing tags between posts, the property tagswill need to be set during the initialization of the object:


const Post = Ember.Object.extend({
  init() {
    this._super(...arguments);
    this.tags = [];
  }
});

Since it initis called every time during a call Post.create(), each instance post will always receive its own array tags. In addition, you can make a tagscomputed property:


const Post = Ember.Object.extend({
  tags: computed({
    return [];
  })
});

Conclusion


Now it’s obvious why you should not write such components as in the example from the beginning of this post. Even if the component appears only once on the page when you exit the route, only the component instance is destroyed, not the factory. So when you return, the new instance of the component will have traces of the previous page visit.


This error can occur when using impurities. Despite the fact that Ember.Mixinthese are not Ember.Objectproperties and methods declared in it, I mix with Ember.Object. The result will be the same: you can ultimately split the state between all objects that use the admixture.


Also popular now: