Ember.js is an open source framework based on the MVVM model for creating complex multi-page applications. Its biggest feature is that it keeps releasing the latest features and doesn’t throw away any old ones.

Unlike most front-end development frameworks, ember.js must follow a strict JS architecture, which means that ember.js does not have a high degree of flexibility. However, thanks to this JS architecture, ember.js is significantly more complete and stable, and can be integrated with the latest releases using tools from any version of ember.js without undue compatibility concerns. This article starts with Ember Data for ember.js.

Ember Data is a powerful set of tools for formatting requests, formalizing responses, and effectively managing local Data caches.

Ember.js itself can be used with any type of backend: REST, JSON:API, GraphQL, or any other backend type.

What is the Ember Data model?

In Ember Data, a model is an object that represents the underlying Data that an application provides to users. Note that although the Ember Data model has the same name as Model, it has a different concept than the Routes method.

Different applications can have very different models, depending on the problem they are trying to solve. For example, a photo-sharing application might have a model that Photo represents a specific Photo, and a model that PhotoAlbum represents a set of photos. In contrast, online shopping applications will likely come in different models, like ShoppingCart, Invoice, or LineItem.

Models tend to be persistent, which means that users don’t expect model data to be lost when they close a browser window. To ensure that no data is lost, if the user makes changes to the model, the model data needs to be stored in a location where it will not be lost.

Typically, most models are loaded and saved to a server that uses a database to store data. Typically, a JSON representation of the model is sent back and forth to a written HTTP server. However, Ember makes it easier to use other persistent storage, such as using IndexedDB to save to a user’s hard drive, or a managed storage solution that avoids writing and hosting your own server.

Once the model is loaded from the store, the component knows how to transform the model data into a UI that users can interact with. For more information on how components retrieve model data, see the “Model with specified routes” guide.

First, working with Ember Data may feel different from the way a JavaScript application works. Many developers are familiar with using Ajax to retrieve raw JSON data from an endpoint, which at first glance seems easy. Over time, however, complexity seeps into the application code, making it difficult to maintain.

With Ember data, managing the model for application growth becomes both simple and easy.

Once you know Ember Data, you’ll have a better way to manage the complexities of Data loading in your applications. This will allow the code to grow and grow and become more maintainable.

Flexibility with Ember Data

Because of the adapter pattern, Ember Data can be configured to work with many different kinds of backends. There is a complete ecosystem of adapters and several built-in adapters that allow Ember applications to communicate with different types of servers.

By default, Ember Data is intended to work with the JSON:API. JSON:API is a formal specification for building a regular, robust, and high-performance API that allows clients and servers to communicate model data.

JSON: the API standardizes how JavaScript applications communicate with the server, so there is less coupling between the front and back ends and more freedom to change the stack.

If you need to integrate an ember.js application with a server that does not have an adapter available (for example, manually scrolling an API server that does not follow any JSON specification), Ember Data can be configured to work with any Data returned by the server.

Ember Data is also intended for use with streaming servers, such as WebSockets powered servers. You can open the server’s socket and push any changes to Ember Data as they occur, giving your application a real-time user interface that is always up to date.

Store and a single source of truth

A common approach to building Web applications is to tightly couple user interface elements with data extraction. For example, suppose you are writing the admin section of your blog application that has a draft feature that lists the currently logged in user.

It may be tempting to make this component responsible for retrieving and storing data during project development:

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";

export default class ListOfDraftsComponent extends Component {
    @tracked drafts;

    constructor() {
        super(...arguments);

        fetch("/drafts").then((data) => {
            this.drafts = data;
        });
    }
}
Copy the code

You can then display the draft list in the component template, as follows:

<ul>
  {{#each this.drafts key="id" as |draft|}}
    <li>{{draft.title}}</li>
  {{/each}}
</ul>
Copy the code

This is useful for the list-of-Drafts component. However, an application can be made up of many different components. On another page, you might want the component to show the draft count. You might want to copy and paste existing willRender code into a new component.

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";

export default class DraftsButtonComponent extends Component {
    @tracked drafts;

    constructor() {
        super(...arguments);

        fetch("/drafts").then((data) => {
            this.drafts = data;
        });
    }
}
Copy the code
<LinkTo @route="drafts" @tagName="button">
  Drafts ({{this.drafts.length}})
</LinkTo>
Copy the code

Unfortunately, the application will now make two separate requests for the same information. The acquisition of redundant data not only wastes bandwidth, but also slows down the perceived speed of the application, and is expensive, and the two values can easily get out of sync. You’ve probably already used a Web application where the list of items is out of sync with the counters in the toolbar, resulting in a frustrating and inconsistent experience.

There is also a close connection between the application’s UI and the network code. If the format of the URL or JSON payload changes, it is likely to break all UI components in a way that is hard to track.

The SOLID principle of good design tells us that objects should have a single responsibility. The component’s responsibility should be to present the model data to the user, not to retrieve the model.

Good Ember apps take a different approach. Ember Data provides a repository that is the central repository for the models in your application. Routes and their corresponding controllers can ask the Store for models, and the Store is responsible for knowing how to get them.

This also means that the Store can detect that two different components are requesting the same model, allowing the application to fetch data from the server only once. The Store can be thought of as a read-through cache for the application model. Routes and their controllers can access the shared storage. When they need to display or modify a model, they first ask store.

Ember Data injects storage services into each route and controller, so it can be accessed through this.store!

Models

In Ember Data, each Model is represented by a subclass that defines the properties, relationships, and behaviors of the Data provided to the user.

The Model defines the type of data that the server will provide. For example, a Person Model might have a firstName string birthday attribute and a date attribute:

import Model, { attr } from "@ember-data/model";

export default class PersonModel extends Model {
    @attr("string") firstName;
    @attr("date") birthday;
}
Copy the code

The Model also describes its relationships to other objects. For example, an order might have multiple line-items, and a line-item might belong to a particular order.

import Model, { hasMany } from "@ember-data/model";

export default class OrderModel extends Model {
    @hasMany("line-item") lineItems;
}
Copy the code
import Model, { belongsTo } from "@ember-data/model";

export default class LineItemModel extends Model {
    @belongsTo("order") order;
}
Copy the code

Models don’t have any data on their own; they define the properties, relationships, and behaviors of a particular instance, called Records.

Records

A Record is an instance that contains a data model loaded from the server. Applications can also create new records and save them back to the server.

A Record is uniquely identified by its model type and ID.

For example, if you are writing a contact management application, you might have a Person model. Individual records in the application may have types person and ID 1 or Steve-buscemi.

this.store.findRecord("person", 1); // => { id: 1, name: 'steve-buscemi' }
Copy the code

The server typically assigns ids to the record the first time it is saved, but ids can also be generated on the client side.

Adapter

One adapter is to transform objects from Ember requests (such as “find user with ID 1”) to transform requests to the server.

For example, if an application requires PersonID of 1, how should Ember load it? HTTP or WebSocket? If HTTP, is it URL /person/1 or /resources/people/1?

The adapter is responsible for answering all these questions. Whenever an application asks the Store for a record that has not been cached, it asks the adapter. If the record is changed and saved, the store hands it over to the adapter to send the appropriate data to the server and confirm that the save was successful.

The adapter lets you completely change how the API is implemented without affecting Ember application code.

Caching

Store will automatically cache records. If a record has already been loaded, the second request always returns the same object instance. This minimizes the number of round trips to the server and allows the application to present its UI to the user as quickly as possible.

For example, the first time an application asks a Store for a record with person ID 1, it will get that information from the server.

However, the next time an application requests a Person ID of 1, the Store will notice that it has already retrieved and cached that information from the server. Instead of sending a request for the same information to other applications, it provides the application with the same record as the first one. This feature, which always returns the same record object no matter how many times it is searched, is sometimes referred to as identity mapping.

Using identity mapping is important because it ensures that changes made in one part of the UI are propagated to the rest of the UI. This also means that you don’t have to keep records synchronized manually – you can ask for records by ID without worrying about whether other parts of the application have already requested and loaded it.

One disadvantage of returning cached records is that you may discover that the state of the data has changed since it was first loaded into the store’s identity map. To prevent long-term problems with this outdated Data, Ember Data automatically makes a request in the background every time a cached record is returned from storage. When new data is entered, the record is updated, and if the record has changed since the initial rendering, the template is rerendered with the new information.

The architecture overview

The first time an application requests a record from store, the Store sees that it has no local copy and requests it from the adapter, which retrieves the record from the persistence layer, which typically will be the record from the HTTP server and represented in JSON format.

As shown in the figure above, the adapter cannot always return the requested record immediately. In this case, the adapter must make an asynchronous request to the server, and only after the request is loaded can records be created with its backup data.

Because of this asynchrony, the store immediately returns a Promise findRecord() from this method. Similarly, storing any request to the adapter will return a Promise.

Once the request to the server returns a JSON payload of the request record, the adapter uses JSON to parse the promise returned to the Store.

The Store then uses the JSON, initializes the record with the JSON data, and uses the newly loaded record to parse the Promise returned to the application.

What if the request stores records already in its cache.

In this case, because the store already knows about the record, it will return a promise that will be resolved immediately with the record. Because it already saves a copy locally, there is no need to ask the adapter (and therefore the server) for a copy.