Ember Apollo Client + @use
Tags: graphql, ember, typescript
Published: 2021-04-13 22:36:48

---
title: Ember Apollo Client + @use 
published: true
description: A real-world application for @pzuraq's new `@use` API in Ember, using it to wrap some common ember-apollo-client methods
tags: graphql, ember, typescript
---

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`](https://github.com/ember-graphql/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:

```js
// 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:

```js
// 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](https://www.pzuraq.com/introducing-use/).

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`](https://github.com/pzuraq/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.

```ts
// 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 GET_TEAMS 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>(() => [{
    GET_TEAMS,
    variables: { sort: this.sort }
  }]));

  @action updateSort(key, dir) {
    this.sort = `${key}:${dir}`;
  }
}
```
And voila! No more setup/ teardown, our `WatchQuery` helper handles all of this for us.

> Note: `valueFor` is a utility function which helps reflect the type of the "value" property exposed on the Resource. More on that below. This utility should soon be exported directly from `ember-could-get-used-to-this`.

So whats going on under the hood?

```ts
// 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';

type QueryOpts = Omit<WatchQueryOptions, 'query'>;

interface WatchQueryArgs {
  positional: [DocumentNode, QueryOpts];
}

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(
      this.args.positional[0].variables
    );
  }

  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:
```ts
setup() {
  taskFor(this.run).perform(); 
}
```

And a way to clean up after ourselves when we're done:
```ts
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:
```ts
update() {
  this.observable?.refetch(
    this.args.positional[1].variables
  );
}
```

## 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 👋