⬆️ ⬇️

Ember - Best Practices: Avoid Leaking Condition into the Factory

In 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 experiences 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 Ember.Object back to the basics of Ember.Object .



Ember.Object is one of the first things we learn as Ember developers, and no wonder. Almost every object we work with in Ember, be it 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 misused:



 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 Ember.Object does 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); 


CoreObject provides 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 the prototype, and then creating new objects by calling new SomeConstructor() . For the ability 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 you often have to use with Ember objects, such as init , create , extend , or reopen , are defined there as well.



Observable is an admixture (Mixin), which allows you to monitor changes in the properties of an object, as well as at the time of get and set calls.



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



New Class Announcement



You can define a new type of observed object by extending the 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 objects of type Post can now be created by calling Post.create() . For each entry, properties and methods declared in the Post class will be inherited:



 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 property values ​​and give the post the name and the name of the author. These values ​​will be set on the instance, not on the class, so they will not affect the posts to 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's stop on this a bit more.



State leaks inside a class



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



 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 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. Because the new value for the tags property was not set, it just mutated the main array. So we efficiently threw the state into the 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 this, of course, is the one that occurs more often. In the following example, you can set the default for createdDate for the current date and time by passing new Date() . But new Date() evaluated once when a 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 tags property will need to be set during object initialization:



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


Since init is called whenever Post.create() called, each post instance will always get its own tags array. In addition, you can make tags computed property (computed property):



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


Conclusion



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



This error may occur when using impurities. Despite the fact that Ember.Mixin is not Ember.Object , the properties and methods declared in it, I Ember.Object to Ember.Object . The result will be the same: you can end up sharing the state between all the objects that use the impurity.



')

Source: https://habr.com/ru/post/318096/



All Articles