Fifth Time’s the Charm – Debugging Ember Octane & Apollo

I recently worked through a seemingly-endless problem inside of a tech stack that I have little experience with. After about half a year inside of a different project that I knew everything about, this process felt pretty painful and frustrating. In fact, I had to experiment with a number of solutions before I found one that worked.

This is extremely common in the world of programming, but blog posts tend to pass over the process of debugging. That’s why I thought it would be fun to share the steps I went through to finally get some working code.

The Problem

This tech stack involves Ember Octane and Ember Apollo Client. One of the great features of Apollo is the ability to re-fetch queries after a mutation happens using the refetchQueries option. My experience in React is that components simply re-render if their HOC’s query is re-run. I expected something similar to happen in Ember (but I was wrong).

I had a route whose model() function calls watchQuery and then returns the awaited result. For this example, we’ll say the query is for a door, which has a boolean isLocked attribute. That model gets passed into the route’s controller as the model property. The controller has a getter, called isDoorLocked, that looks at the inner boolean property (model.door.isLocked).

The controller also has an action called unlockDoor that calls mutate and does what you would expect on the server. The template renders a button that calls the unlockDoor action when clicked. I pass the refetchQueries option to mutate because I expect it to alter the door. My assumption is that when the query is re-fetched, isDoorLocked‘s output will change and cause the template to update in my browser. Unfortunately, that is not the case. Even though I can see in the DevTools‘ network tab that the query is re-executing with the expected response, the template still says the door is locked.

It took quite a bit of trial and error, and just as much Chrome DevTools, to eventually solve this problem.

First Try

After reading a bit about the new patterns that Octane is establishing, I first tried to solve the problem with a @tracked decorator. Tracked properties are meant to solve the same problem as computed properties, but with a more simple design. Since model is the only property on the controller, I tried adding the @tracked decorator to it:

@tracked model;

This seemed like a plausible solution—the route’s model() function calls watchQuery(), so the refetchQueries should force the watchQuery to update the model.

But the @tracked component didn’t seem to have an effect. I used good ol’ console.log() and discovered that while the query result inside of model had changed after the query refetch, the model itself was unchanged. Tracking the model did not cause the isDoorClosed function to re-run.

Second Try

The refetch updated a property inside the model, but not the model object itself. So maybe I could just return the property? I didn’t need any other properties inside of model anyways. I changed the route’s model() function to return this:

return (await this.apollo.watchQuery({ query, variables })).door;

But that didn’t solve the problem. The model function is only called once, so while the refetch did update the door property somewhere, the controller had no way to access it.

Third Try

At this point, I was still convinced that I was misusing the model() function. I did some more reading about Ember routes and models and the watchQuery function. One thing that immediately jumped out to me was a code snippet in the Ember Apollo Client’s README:

model() {
    let result = this.apollo.watchQuery(...);
    let observable = getObservable(result);
    observable.fetchMore(...) // utilize the ObservableQuery
    ...
  }

The preceding paragraph said getObservable() is used for scenarios like pagination, which was not my goal. On the other hand, some extra observability seemed like a good thing.

I changed my code to match the example from the README, then used Ember Inspector to look inside the controller’s new model. None of its properties caught my attention, other than lastResult and observers. I didn’t have experience with observers and wasn’t sure how to actually use this object, so I looked into Ember’s Observable mixin and tried to use addOberver.

Long story short… this did not work. I think the problem is that Apollo’s observables and Ember’s observables don’t simply work together automagically. Not a big surprise. My lack of experience with observables in general could also be a factor, but this experiment seemed like a strong enough dead-end that I quickly moved on.

Fourth Try

At this point, I was starting to doubt that using a watchQuery in the route and mutation in the controller was how Ember Apollo Client is meant to work at all. But the documentation didn’t provide any full examples, so it seemed pointless to do something drastic like resorting to Ember components to use Apollo. My next strategy was to make Ember re-calculate isDoorUnlocked.

Once again resorting to tracked properties, I added one to the controller named counter with a default of 1. Then I added an incrementation to counter right after the mutation runs and calls counter in isDoorLocked. Since Ember will add a tracked property to the autotrack stack whenever its getter is called, I didn’t need to actually do anything with counter at that spot!

With fingers crossed, I reloaded the page and triggered unlockDoor and then… (!!!)

Nothing. The template still wasn’t updating, even though a console.log inside of isDoorLocked showed that counter was incrementing and causing the getter to re-calculate. The log also showed that model.door.locked was true, even though the query result showed in the network tab showed that locked: false. What’s more, the model.door.locked was false according to the Ember DevTools.

Smelled like a race condition. I double-checked that the code had await everywhere that it should. I tried to learn about how Ember goes about re-calculating getters that reference tracked properties. No cigar.

Fifth Try

Eventually I started to look into the updateQueries option on mutate, just for the sake of hooking into the state of Apollo, and there it was — awaitRefetchQueries, an option to finish refetching queries before mutate returns. Until I saw this option, I hadn’t even realized that mutate doesn’t wait for refetches.

Adding awaitRefetchQueries finally caused the template to re-render. It was a long process, and using counter was not the kind of solution I was expecting, but it worked.

Takeaways

Coming up with a working solution took way longer than I’d like to admit. The whole time, I kept thinking, “This would be so much easier with React,” and “Why are we using Apollo with Ember when there aren’t complete examples in GitHub, so I can copy and paste everything?” That’s an exaggeration, but it really was a painful process.

After I finally had a solution, it occurred to me that there were some hidden payoffs. I had learned quite a bit about Ember Octane and about older Ember patterns in the process. I learned about Ember Apollo Client and the core Apollo Client library, plus the way those two are related. Surprisingly, I even learned a little more about React Apollo Client.

I learned how much I don’t know about observables, so I’ll need to do some more learning at a later time. I got some practice with using Ember DevTools and Chrome DevTools more generally. Even more importantly, I painfully learned (or re-learned, since this happens every once in a while) the consequences of working through a problem haphazardly and without any forethought or strategy.

Programming can be frustrating, but taking the time to solve hard problems is always worth it in the long run!

Edit: Sixth (Bonus) Try

As luck would have it, Ember Octane has a built-in solution to this problem that I discovered after writing this post! Previous versions of Ember provided well-known “computed” properties, which are calculated once and then cached until a dependency changes. I knew about computed properties, but thought that they had been decommissioned in Octane and replaced with get properties. While get properties do solve the same problems as computed properties, Octane provides a @computeddecorator for get properties. This decorator allows you to add dependencies that aren’t actually referenced within the get property. This means that we can use specific properties inside of model – in this case model.door – as dependencies and get rid of the awkward counter property altogether.