ember-apollo-client
Use apollo-client and GraphQL from your Ember app.
This addon includes the following dependencies:
I have been using the non-addon version of this in my own app for a few months. Because I've actually used it to build a real app, I've encountered and solved a few real-world problems such as reliable testing and preventing resource leaks by unsubscribing from watch queries.
Installation
ember install ember-apollo-client
Compatibility
This addon is tested against the release
, beta
, and canary
channels, as
well as the latest LTS.
Configuration
In your app's config/environment.js
, configure the URL for the GraphQL API:
var ENV = {
...
apollo: {
apiURL: 'https://test.example/graphql'
},
...
}
Additional configuration of the ApolloClient can be done by extending the Apollo
service and overriding the clientOptions
property. See the
Apollo Service API for more info.
Usage
Fetching data
The addon makes available an apollo
service. Inject it into your routes and
you can then use it:
app/gql/queries/human.graphql
query human($id: String!) {
human(id: $id) {
name
}
}
app/routes/some-route.js
import Ember from 'ember';
import UnsubscribeRoute from 'ember-apollo-client/mixins/unsubscribe-route';
import query from 'my-app/gql/queries/human';
export default Ember.Route.extend(UnsubscribeRoute, {
apollo: Ember.inject.service(),
model(params) {
let variables = { id: params.id };
return this.get('apollo').query({ query, variables }, 'human');
}
});
When you use the query
method, ember-apollo is actually performing a
watchQuery
on the ApolloClient. The resulting object is an Ember.Object
and
therefore has full support for computed properties, observers, etc.
If a subsequent query (such as a mutation) happens to fetch the same data while this query's subscription is still active, the object will immediately receive the latest attributes (just like ember-data).
Please note that when using query
, you should unsubscribe when you're done
with the query data. You can instead use queryOnce
if you just want a single
query with a POJO response and no watch updates.
See the API docs for more details.
Mutations and Fragments
You can perform a mutation using the mutate
method. You can also use GraphQL
fragments in your queries. This is especially useful if you want to ensure that
you refetch the same attributes in a subsequent query or mutation involving the
same model(s).
The following example shows both mutations and fragments in action:
app/gql/fragments/review-fragment.graphql
fragment ReviewFragment on Human {
stars
commentary
}
app/gql/mutations/create-review.graphql
#import 'my-app/gql/fragments/review-fragment'
mutation createReview($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
review {
...ReviewFragment
}
}
}
app/routes/my-route.js
import Ember from 'ember';
import mutation from 'my-app/gql/mutations/create-review';
export default Ember.Route.extend({
apollo: Ember.inject.service(),
model() {
return Ember.Object.create({});
},
actions: {
createReview(ep, review) {
let variables = { ep, review };
return this.get('apollo').mutate({ mutation, variables }, 'review');
}
}
});
Apollo service API
The apollo
service has the following public API:
-
clientOptions
: This computed property should return the options hash that will be passed to theApolloClient
constructor. You can override this property to configure the client this service uses:const OverriddenService = ApolloService.extend({ clientOptions: computed(function() { let opts = this._super(...arguments); return merge(opts, { dataIdFromObject: customDataIdFromObject }; }), });
-
middlewares
: This computed property provides a list of middlewares to the network interface. You can use the macromiddlewares
to create your middlewares:import middlewares from 'ember-apollo-client/utils/middlewares'; const OverriddenService = ApolloService.extend({ middlewares: middlewares('authorize'), authorize(req, next) { // Authorization logic next(); } });
Or create them on your own:
const OverriddenService = ApolloService.extend({ middlewares: computed(function() { return [ { applyMiddleware: (req, next) => this.authorize(req, next) } ]; }), authorize(req, next) { // Authorization logic next(); } });
-
query(options, resultKey)
: This calls theApolloClient.watchQuery
method. It returns a promise that resolves with anEmber.Object
. That object will be updated whenever thewatchQuery
subscription resolves with new data. As before, theresultKey
can be used to resolve beneath the root.When using this method, it is important to unsubscribe from the query when you're done with it.
-
queryOnce(options, resultKey)
: This calls theApolloClient.query
method. It returns a promise that resolves with the raw POJO data that the query returns. If you provide aresultKey
, the resolved data is grabbed from that key in the result. -
mutate(options, resultKey)
: This calls theApolloClient.mutate
method. It returns a promise that resolves with the raw POJO data that the mutation returns. As with the query methods, theresultKey
can be used to resolve beneath the root.
Unsubscribing from watch queries
Apollo client's watchQuery will continue to update the query with new data whenever the store is updated with new data about the resolved objects. This happens until you explicitly unsubscribe from it.
In ember-apollo-client, this is exposed on the result of query
via a method
_apolloUnsubscribe
. You should call this method whenever you're done with the
query. On a route, this can be done with the resetController
hook. In a
component, this cleanup is typically done with a willDestroyElement
hook.
To make this easier on routes, this addon also provides a mixin called
UnsubscribeRoute
. You can use it in your route like this:
import Ember from 'ember';
import UnsubscribeRoute from 'ember-apollo-client/mixins/unsubscribe-route';
export default Ember.Route.extend(UnsubscribeRoute, {
model() {
return this.get('apollo').query(...);
}
});
The mixin will call _apolloUnsubscribe
on the model
(if it is set) when the
model changes or the route deactivates. For now, this only works if your model
was resolved directly from the apollo service. It does not work if your model
hook returns an RSVP.hash
of multiple queries, or something of that sort.
You'd have to clean up manually in that scenario.
Injecting the apollo service into all routes
The apollo service is not automatically injected into your routes, but you can do so easily with an initializer like this one:
export function initialize(application) {
application.inject('route', 'apollo', 'service:apollo');
}
export default {
name: 'apollo',
initialize
};
Testing
This addon is test-ready! All promises from the apollo service are tracked with
Ember.Test.registerWaiter
, so your tests should be completely deterministic.
The dummy app contains example routes for mutations and queries:
The tests also contain a sample Star Wars GraphQL schema with an ember-cli-pretender setup for mock data.
Development
Installation
git clone https://github.com/bgentry/ember-apollo-client
this repositorycd ember-apollo-client
npm install
bower install
Running
ember serve
- Visit your app at http://localhost:4200.
Running Tests
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
Building
ember build
For more information on using ember-cli, visit https://ember-cli.com/.
Contributors
A special thanks to the following contributors:
- Dan Freeman (@dfreeman)
- VinÃcius Sales (@viniciussbs)
- Laurin Quast (@n1ru4l)