Developing with Ember.js, we are used to dealing with the Store service which many of us associate with Ember Data. It was while I was doing some research on how Ember integrates with data layers that I realised that the Store is part of an integration glue that exists in Ember even when Ember Data is not present. Moreover, you can create a light integration with a data layer by simply hooking into Ember's Store interface.
Here's a little guide for you to experiment with the possibilities.
Ember's minimal Store interface
First, to make sure we are not using Ember Data accidentally, let's remove it
from our app. Simply remove the appropriate line from your packages.json
and
rerun npm install
(or yarn install
):
1diff --git a/package.json b/package.json 2index 5ba44f8..c536b45 100644 3--- a/package.json 4+++ b/package.json 5@@ -32,7 +32,6 @@ 6 "ember-cli-shims": "^1.2.0", 7 "ember-cli-sri": "^2.1.0", 8 "ember-cli-uglify": "^2.0.0", 9- "ember-data": "~3.1.0", 10 "ember-export-application-global": "^2.0.0", 11 "ember-load-initializers": "^1.0.0", 12 "ember-maybe-import-regenerator": "^0.1.6",
So now we know there's no Ember Data. Next, let's say we have declared a route that looks like follows:
As well as this template:
If we do not provide a route module, Ember will provide one for us, using a
default implementation. This will see the :user_id
parameter last in the
path, and from there it will figure out that we are expecting to retrieve a
model called user
, and its ID will be given in the URL. Roughly it will
work like this:
Note that this uses this.store.find()
instead of this.store.findRecord()
!
Why is this? Well, there's a difference between Ember Data's interface
and Ember's protocol to interact with a data layer.
Ember can integrate automatically with a data layer in this small way, providing
a default route module and figuring out the model name and record id
from the path. From here, a clever data layer can provide the appropriate integration
hooks, and Ember will use them. In this case the hook is a store
service which
provides a find
method.
But why find
and not findRecord
? Because Ember and Ember Data are
independent. Long time ago it was established that this method would be called
find
, and initially Ember Data used find
instead of findRecord
.
Eventually Ember Data moved on to a new interface, bringing the methods that
we use nowadays. However Ember didn't need to follow suit, and anyway there
were already other data layers that were implementing this interface already
and there was no point on breaking their integrations. Ember Data does provide
a find
method which simply translates to findRecord
internally, and all
works as expected.
The default store
But anyway, as I was saying, somewhere in our app we have the following code:
1this.store.find('user', params.user_id);
When Ember Data is not present, Ember provides a default store. This store
assumes that we have defined our models (in this case, a user
model) in
files living at app/models
. This would be an example:
(To be precise, Ember doesn't care about the location and name of this file,
or even if there's a file. The important part is that the object above
is registered as model:user
in the dependency injection container, but
that's a story for another day.)
This initial model is not very useful, returning a hard-coded object. This second approach would work with a simple REST API:
This approach is promising, but it has one big issue: we cannot use
dependency injection in objects that we instantiate this way. For
example, if instead of fetch
we wanted to use the ajax
service, or
if we wanted to grab configuration details from another service, we'd
be out of luck.
A custom store
To allow our custom models to play with the injection container, the simplest way might be to do just like Ember Data does, and provide our own store service.
This is an example of a custom store service:
1/// app/services/store.js 2import Service, { inject } from '@ember/service'; 3import { pluralize } from 'ember-inflector'; 4 5export default Service.extend({ 6 ajax: inject(), 7 find(model, id) { 8 const resourcePath = pluralize(model); 9 return this.ajax.request(`/${resourcePath}/${id}`); 10 }, 11});
Then, to get this custom store injected by default in our routes, we can register it with the container in an initializer:
And voila: your routers, default or otherwise, will be using your custom store.
This is the same approach used by Ember Data which, at the time of writing
these lines, does exactly this: it injects its own store into routes,
controllers, etc, which is then used transparently by us developers.
See addon/setup-container.js
on the Ember Data source code to check for yourself.