A coworker and I recently encountered a seemingly innocuous Ember route and a
misbehaving Ember app. Nearly convinced it was an Ember bug we stepped back and
questioned our assumptions. After journeying through Ember’s docs and source
code we discovered that the apparent bug was actually intended behavior. It
turns out that Ember’s Route.setupController
hook behaves a little differently
if it’s invoked with an undefined
model rather than a null
one.
This post walks through the counterintuitive behavior we encountered and the
steps we took to find out what was really going on.
I’ll start by setting up an Ember twiddle that exposes the seemingly buggy behavior.
Recreating the bug with Ember Twiddle
Here’s the ember twiddle
I’ll be using in this post. It’s contrived to illustrate the problem
so admittedly the code will feel a little funky.
The twiddle app will display recipes and ingredients for different types of
foods. You’ll notice a ‘foods’ and ‘recipes’ segment in the url at the top.
Those two url segments correspond to a set of routes:
From the routing, the ‘recipes’ route and UI are both nested inside the ‘foods’:
Clicking on a food type on the left will add a query param and filter the recipes:
You can also go to an ingredients page for each type of food:
The problem
Clicking on a food type to filter, then clicking back to ‘All foods’ will clear
out the query param, but the filter on recipes stays the same.
Where to begin? Start at the template
When presented with behavior I’m not expecting, I always start at the template if the app is rendering
correctly. From there I pretty much follow a flowchart
to avoid jumping to any conclusions. Starting in the templates also means I can have almost zero knowledge
about the app itself and still debug like a champ!
In the template the {{recipeName}}
and filteredRecipes
values both don’t
match what I would’ve expected them to be. Since this is a controller-backed
template those values both had to be set on the controller at some point.
Both of those are computed properties that depend on 'model'
, which isn’t set directly anywhere in this file. I’ll assume
that ‘model’ is set on the controller by its corresponding route, in this case routes/foods/recipes.js
The model hook is short, at least. this.modelFor('foods')
(api docs)
is getting the return value of the foods
route’s model hook. Since foods
is
a parent route, I know that by the time the recipes
route runs foods
will
already have its model so this looks fine. My first assumption is that the
model hook must not be running when the foodId
query param isn’t set (for
whatever reason), but to confirm it I’ll stick a console log into it and click around.
Completely wrong! The model hook runs every time. I must be missing something.
It’s also worth pointing out that the model hook doesn’t actually set the
model on the controller. That’s the job of the (aptly named) setupController
hook, which in this route is just the default implementation. It’s mentioned briefly
at the end of the routing guide,
but it’s not a hook I end up having to use too often. Maybe that’s not
running? I’ll implement the hook myself and go from there.
Gah! My version of the setupController
hook works as I’d expect. How does Ember’s implementation
differ? It’s worth pointing out that Ember’s API docs are quite good at this point, and there are
links to the source of each method. Here’s the code on github linked from the
setupController docs.
And there is the assumption that I didn’t know I’d made: the default
implementation of setupController doesn’t set the model if the model hook
returns ‘undefined’, which is not what I’d expected. At this point I have two
ways to fix my problem: I could keep the setupController
function I’ve
defined, or I could make the model hook return null
instead of undefined
.
Stepping back
The process we used to find out what was going on reinforced a good rule of
thumb when it comes to debugging: Start with your code first. I think my code
is doing X.
It’s much easier to deal with code you wrote first, and then move onto the
code you didn’t write, rather than the other way around. This helps put all of
the assumptions you have about your code out on the table. With any framework
there’s a macro set of assumptions that come with it and validating (or
invalidating) assumptions on your code will give you a more direct route.
In this particular example we thought one of Ember’s route hooks was going to
behave a certain way. It did except for the case where it was dealing with an
undefined
value. Who would have thought undefined
would behave
differently?
When understanding the expected behavior the Ember guides and the API docs are
the best places to start. When what you’re looking for isn’t mentioned the next
place to go is the test suite.
For example, Ember Data has a great test suite that has helped clarify how to
use things like
polymorphism
that weren’t in the guides until
recently.
Lastly, the Ember source code is great! Things are clearly packaged up, and in a
pinch the whole thing is right there in your dev tools. As you run into
problems, make sure you’re not incorrectly assuming what should be happening
before going down the rabbit hole.