[002.3] The Model Hook and Ember Data Store

Ember Data, explained a bit.

Subscribe now

The Model Hook and Ember Data Store [09.12.2016]

We are going to go over Ember Data's store and its methods for getting data. We will also looks at some of the common issues that new Ember developers might run into with these methods.

Model Hook

If you remember back in episode 001.4, we set up a model hook in our contacts route that looked like this:

export default Ember.Route.extend({
  model(){
    let contact1 = { id: 1, firstName: 'Josh', lastName: 'Adams', email: 'josh@dailydrip.com'};
    let contact2 = { id: 2, firstName: 'Adam', lastName: 'Dill', email: 'adam@dailydrip.com'};

    return [contact1, contact2];
  }
});

In Ember, the model hook on a given route attaches whatever data it returns to its controller. This data is accessible in the controller as the model property (e.g. this.get('model')). This is the most common means of supplying data to our application.

In the example above, we set our model hook to return an array of two JavaScript objects. Plain JavaScript objects and arrays work fine here. We can also just make ajax calls in the model hook to our server and return the payload. On occasion, you might need to structure your app that way.

However, in most cases we will benefit from using Ember Data to manage our models and data. Ember Data gives us a more ergonomic means of getting, updating, and deleting server data. First, we would set up a model and an adapter (we'll walk through the details of that a little later). Then we have access to Ember Data's store methods.

An example would look like this:

model(){
  return this.store.findAll('contact');
}

The Store

The store is a service that is automatically injected into Ember routes and controllers. We can see how it works as a service by looking in a component. If we had a contacts model and we were to add the following to a component:

contacts: Ember.computed(function(){
  const store = this.get('store');
  return store.findAll('contact');
}).volatile()

Then, when we called {{#each contacts as |c|}}{{c.firstName}}{{/each}} in the component's template we would get an error.

Uncaught TypeError: Cannot read property 'findAll' of undefined

The store is not automatically injected into components. However, if we change the component above to look like this:

store: Ember.inject.service(), // the name of the service is inferred from the property name
contacts: Ember.computed(function(){
  const store = this.get('store');
  return store.findAll('contact');
}).volatile()

This will work. The Store provides a single source of truth for your data. This helps prevent fetching the same data twice and prevents having the same data living across different components getting out of sync.

Ember Data provides several methods for retrieving models from the store. We'll spend the rest of our time here going over them and documenting a few tricky issues that you might encounter.

Fetching Data

  • this.store.findAll('contact') - returns a promise that should resolve to all records of the contact type.
  • this.store.peekAll('contact') - returns all records of a contact type that are already loaded in the store, this will not make an api call to your server.
  • this.store.findRecord('contact', 1) - returns a promise that should resolve to a contact record with an id of 1
  • this.store.peekRecord('contact', 1) - returns a contact record with an id of 1 if it is present in the store, if it is not present in the store, this method returns null.

The other two primary methods of data retrieval are the query methods.

  • this.store.query('contacts', { name: 'Brandon' }) - this sends the second parameter, an object, to the server as query parameters and returns a promise that should resolve to a RecordArray. The api call might look something like: http://apiserver.com/api/contacts?name=josh.
  • this.store.queryRecord('contacts', { email: 'brandon@someemail.com'}) - this sends the query object to the server as query parameters and can be used when we are querying a unique property that will only ever return one result, an obvious example being a unique email.

There are several subtle differences in these methods that might not be obvious at first. We'll go over a few.

Live array

The findAll method (this.store.findAll('contact')) returns a live array of contacts from the store. So, if you add an object of the contact type to the store, the array will automatically update with the new object present.

If, however, you use this.store.query('contact', {firstName: 'bob'}) you do not get a live array. Even if you create a new record that matches the query, your array will not be updated to include the new object.

find vs peek

By default, if you use this.store.findRecord('contact', 1) and a contact record with an id of 1 is present in your store, the promise will resolve with the local version of the record. In the background, Ember will still make an API call to your server and will update the data when the api call completes. By contrast, this.store.peekRecord('contact', 1) will immediately return either the record from the store, or it will return null. A peek method will never fire an API call.

Route's model hook

If you are transitioning to a new route and you pass in an object to the transition method, the model hook on the route you are entering will not fire. Ember assumes that it has all the information it needs to complete the transition without using the model hook. For example, using the link-to helper:

{{#link-to 'contacts.contact' contact}}
  {{contact.firstName}}
{{/link-to}}

Here, Ember will use the contact object that was passed in as the model, without firing the model hook on the router. If we want to force the model hook to fire, we should pass in the id of the object, rather than the object itself. Example:

{{#link-to 'contacts.contact' contact.id}}
  {{contact.firstName}}
{{/link-to}}

Resources

Prep for moving forward

That is it for now, tomorrow we will put some of this into practice by hooking our app to a server and implementing a model and adapter.

I created a very basic node app that will provide a suitable backend for Ember.

This is a very simple server implementation that will default to running at http://localhost:4000. As has been mentioned before, Ember is an opinionated framework. One of the consequences of this is that if you do things Ember's way, it can be very easy. One of the conventions that Ember has embraced is the json api spec. The app listed above is serving fixtures as a JSON API compliant payload.

For tomorrow it would be helpful to have that app cloned and running locally.