Ember Apollo Client + @use

I’ve recently spun up my first Ember app using GraphQL, and as I would do when approaching any new functionality in my Ember app, I reached for the community supported addon ember-apollo-client.

ember-apollo-client provides a really nice wrapper aroun…


This content originally appeared on DEV Community and was authored by Chris Miller

I've recently spun up my first Ember app using GraphQL, and as I would do when approaching any new functionality in my Ember app, I reached for the community supported addon ember-apollo-client.

ember-apollo-client provides a really nice wrapper around everything I'd want to do with the @apollo/client, without making too many assumptions/ abstractions. It nicely wraps the query, watchQuery, and subscribe methods, and provides a queryManager for calling those methods, which quite nicely cleans them up for you as well.

Ember traditionally has many ways to set up/ clean up data-fetching methods, and you usually fall into two camps; I find myself choosing a different path almost every time I write an ember app.

1. Use the model hook

ember-apollo-client first suggests using your model hook, illustrated here:

// app/routes/teams.js
import Route from '@ember/routing/route';
import query from '../gql/queries/teams';

export class TeamsRoute extends Route {
  @queryManager apollo;

  model() {
    return this.apollo.watchQuery({ query }, 'teams');
  }
}

Pros: This method is well supported by the framework, and allows for utilizing error and loading substates to render something while the model is reloading.

Drawbacks: query parameters. Say we have a sort parameter. We would then set up an additional observable property within our model hook, and likely use the setupController hook to set that on our controller for re-fetching data when sort changes. This is fine, but includes extra code which could become duplicative throughout your app; leading to potential bugs if a developer misses something.

2. Utilize ember-concurrency

Based on a suggestion I found while digging through their issues and documentation, I gave ember-concurrency a shot:

// app/routes/teams.ts
import Route from '@ember/routing/route';

export class TeamsRoute extends Route {
  setupController(controller, model) {
    controller.fetchTeams.perform();
  }

  resetController(controller) {
    controller.fetchTeams.cancelAll();
    unsubscribe(controller.fetchTeams.lastSuccessful.result);
  }
}

// app/controllers/teams.js
import Controller from '@ember/controller';
import query from '../gql/queries/teams';

export class TeamsController extends Controller {
  @queryManager apollo;
  @tracked sort = 'created:desc';

  @task *fetchTeams() {
    const result = yield this.apollo.watchQuery({ 
      query, 
      variables: { sort: this.sort } 
    });

    return {
      result,
      observable: getObservable(result)
    };
  }

  @action updateSort(key, dir) {
    this.sort = `${key}:${dir}`;
    this.fetchTeams.lastSuccessful.observable.refetch();
  }
}

Pros: This feels a little more ergonomic. Within the ember-concurrency task fetchTeams, we can set up an observable which will be exposed via task.lastSuccessful. That way, whenever our sort property changes, we can access the underlying observable and refetch.

ember-concurrency also gives us some great metadata and contextual state for whether our task's perform is running, or if it has errored, which allows us to control our loading/ error state.

Drawbacks: In order to perform, and subsequently clean this task up properly, we're going to need to utilize the route's setupController and resetController methods, which can be cumbersome, and cleanup especially is easily missed or forgotten.

This also requires the developer writing this code to remember to unsubscribe to the watchQuery. As the controller is a singleton, it is not being torn down when leaving the route, so the queryManager unsubscribe will not be triggered. Note: if this is untrue, please let me know in the comments!

Either way, we will still need to cancel the task. This is a lot to remember!

Enter @use

Chris Garrett (@pzuraq ) and the Ember core team have been working towards the @use API for some time now. Current progress can be read about here.

While @use is not yet a part of the Ember public API, the article explains the low-level primitives which, as of Ember version 3.25+, are available to make @use possible. In order to test out the proposed @use API, you can try it out via the ember-could-get-used-to-this package.

⚠️ Warning -- the API for @use and Resource could change, so keep tabs on the current usage!

How does this help us?

Remember all of those setup/ teardown methods required on our route? Now, using a helper which extends the Resource exported from ember-could-get-used-to-this, we can handle all of that.

Lets go ts to really show some benefits we get here.

// app/routes/teams.ts
import Route from '@ember/routing/route';

export class TeamsRoute extends Route {}

// app/controllers/teams.ts
import Controller from '@ember/controller';
import { use } from 'ember-could-get-used-to-this';
import query from '../gql/queries/teams';
import { GetTeams } from '../gql/queries/types/GetTeams';
import { WatchQuery } from '../helpers/watch-query';
import valueFor from '../utils/value-for';

export class TeamsController extends Controller {
  @tracked sort = 'created:desc';

  @use teamsQuery = valueFor(new WatchQuery<GetTeams>(() => [{
    query,
    variables: { sort: this.sort }
  }]));

  @action updateSort(key, dir) {
    this.sort = `${key}:${dir}`;
  }
}

Note: The valueFor utility function helps us infer the correct value type on our controller for our WatchQuery resource. It should be available from the ember-could-get-used-to-this package soon.

And voila! No more setup/ teardown, our WatchQuery helper handles all of this for us.

So whats going on under the hood?

// app/helpers/watch-query.ts
import { tracked } from '@glimmer/tracking';
import { Resource } from 'ember-could-get-used-to-this';
import { queryManager, getObservable, unsubscribe } from 'ember-apollo-client';
import { TaskGenerator, keepLatestTask } from 'ember-concurrency';
import ApolloService from 'ember-apollo-client/services/apollo';
import { ObservableQuery, WatchQueryOptions } from '@apollo/client/core';
import { taskFor } from 'ember-concurrency-ts';

interface WatchQueryArgs {
  positional: [WatchQueryOptions];
}

export class WatchQuery<T> extends Resource<WatchQueryArgs> {
  @queryManager declare apollo: ApolloService;

  @tracked result: T | undefined;
  @tracked observable: ObservableQuery | undefined;

  get isRunning() {
    return taskFor(this.run).isRunning;
  }

  get value() {
    return {
      result: this.result,
      observable: this.observable,
      isRunning: this.isRunning,
    };
  }

  @keepLatestTask *run(): TaskGenerator<void> {
    const result = yield this.apollo.watchQuery<T>(this.args.positional[0]);

    this.result = result;
    this.observable = getObservable(result);
  }

  setup() {
    taskFor(this.run).perform();
  }

  update() {
    this.observable?.refetch();
  }

  teardown() {
    if (this.result) {
      unsubscribe(this.result);
    }

    taskFor(this.run).cancelAll({ resetState: true });
  }
}

Lot going on, lets break it down:

We've brought in some libraries to help with using typescript, including ember-concurrency-ts.

The Resource class gives us a way to perform our task upon initialization:

setup() {
  taskFor(this.run).perform(); 
}

And a way to clean up after ourselves when we're done:

teardown() {
  if (this.result) {
    unsubscribe(this.result);
  }

  taskFor(this.run).cancelAll({ resetState: true });
}

And remember how we declaratively called refetch after updating sort? Well, now we can utilize ember's tracking system, since we passed sort in the constructor function, it should reliably trigger the update hook if updated:

update() {
  this.observable?.refetch();
}

Where do we go from here

From here, you can use the same paradigm to build out Resources for handling apollo.subscribe and apollo.query, with few code changes.

As our app is very new, we plan on tracking how this works for us over time, but not having to worry about setting up/ cleaning up queries for our application should greatly improve the developer experience right off the bat.

An important thing to note, this article focuses on wrapping the ember-apollo-client methods, but can Easily be extrapolated to support any data-fetching API you want to use, including Ember Data.

Thanks for reading! Please let me know what ya think in the comments ?


This content originally appeared on DEV Community and was authored by Chris Miller


Print Share Comment Cite Upload Translate Updates
APA

Chris Miller | Sciencx (2021-04-13T22:36:48+00:00) Ember Apollo Client + @use. Retrieved from https://www.scien.cx/2021/04/13/ember-apollo-client-use/

MLA
" » Ember Apollo Client + @use." Chris Miller | Sciencx - Tuesday April 13, 2021, https://www.scien.cx/2021/04/13/ember-apollo-client-use/
HARVARD
Chris Miller | Sciencx Tuesday April 13, 2021 » Ember Apollo Client + @use., viewed ,<https://www.scien.cx/2021/04/13/ember-apollo-client-use/>
VANCOUVER
Chris Miller | Sciencx - » Ember Apollo Client + @use. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/04/13/ember-apollo-client-use/
CHICAGO
" » Ember Apollo Client + @use." Chris Miller | Sciencx - Accessed . https://www.scien.cx/2021/04/13/ember-apollo-client-use/
IEEE
" » Ember Apollo Client + @use." Chris Miller | Sciencx [Online]. Available: https://www.scien.cx/2021/04/13/ember-apollo-client-use/. [Accessed: ]
rf:citation
» Ember Apollo Client + @use | Chris Miller | Sciencx | https://www.scien.cx/2021/04/13/ember-apollo-client-use/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.