Why should I not use observers in my ember application?

The guides have a whole page on observers but caveat their use:

Note: Observers are often over-used by new Ember developers. Observers are used heavily within the Ember framework itself, but for most problems Ember app developers face, computed properties are the appropriate solution.

I would like to hear better arguments why application developers should avoid observers. I’m aware of @stefan’s fine talk on the subject but have not watched it. Interestingly, @acorncom mentions linking to the talk for the guides in an archived issue. I look forward to the shimmering land of Octane-happiness but prefer examples for today’s applications.

reasons why not

  1. creates an ambiguous data flow contrary to the DDAU (Data Down Actions Up)
  2. often leads to using setters or imperatively maintained state when most uses just need to derive values ( à la decorators)

Hopefully this post can spark constructive conversation for making the guides more robust.

1 Like

_I don’t have a lot of time to write right now, but additional thoughts on observers (and shifting them to async by default) can be found here: https://github.com/emberjs/rfcs/blob/master/text/0494-async-observers.md_

Of particular interest to this question is the “action at a distance” aspect. When combined with observers historically being synchronous, life can get quite painful in a hurry with observers.

1 Like

As an application grows, if there are no clear rules about what can depend on what, any potential change to your data can propagate in surprising ways. The chains of dependencies get longer and you lose track of them, and before you know it some of the paths form circles.

Once there are circles, there’s no longer necessarily any definitive “rest state” for the system. Things begin to depend heavily on timing – if this happens to update faster than that, we get one answer, otherwise we get another answer (or an Exception). Plus you now probably have performance problems, because a single change may chase itself around the loop repeatedly. And simple changes to one part of the system could break a far-away part of the system, because they alter the global timing!

So circular data dependencies are bad. And seeing the entire cycle is hard, because it requires you to have global understanding of the program. You can’t look at any one component or family of components in isolation to know they are safe. Effects are non-local. Spooky action at a distance.

What we need instead is a rule that can be enforced locally that defends us globally against cycles. That rule is “data down, actions up” (DDAU).

We can break DDAU into the four terms:

  • Data: a way of propagating changes through your program by updating a variable to a new value
  • Down: moving further away from the “top” of the application (which is the outermost stuff in application.hbs), and toward the leafmost components.
  • Action: a way of propagating changes through your program by calling a function
  • Up: the opposite of down. Moving toward the top of the application.

Notice that both “data” and “action” are “ways of propagating changes”. In one sense, they’re equivalent. You could in principle make all communication between components be “data”, or you could make all communication between them be “actions”, and both systems would be equivalently powerful.

But we split them into these two separate categories because it helps us establish our cycle-breaking local rule. The rule, broken down into explicit detail, is these invariants:

  1. You may only invoke actions upward. Never downward.
  2. You may only propagate data changes downward. Never upward.
  3. Actions may cause more actions.
  4. Data changes may cause more data changes.
  5. Actions may cause data changes.
  6. Data changes may not cause actions.

If you maintain these invariants, no cycles can occur. If you break any one of them, you can get a cycle again. And you can examine an individual component to see if it’s breaking any of the invariants – you don’t need to examine the entire global program.

Now, this sets me up to finally answer the original question: the point of observers is usually to break invariant number 6.

I say usually because there are still legitimate uses for observers. They are legitimate for reflecting the state of Ember onto foreign interfaces that don’t understand Ember’s data flow conventions. That doesn’t cause cycles as long as those foreign interfaces aren’t looping back and propagating those changes back into Ember.

The remaining subtleties mostly come from knowing which way is “up” when you’re dealing with more than just a component hierarchy. For example, if two services are interacting with each other, which is “up”? For any given piece of state, you need to decide who “owns it”, and they are the “the top”, and actions get sent to them and everybody else consumes their data.

18 Likes

Thank you for the comprehensive response. It has cleared conceptual fog in how I think about data flow in SPAs, ember or otherwise.

So this confusing indirection is analgous to two-way data binding?

Thank you for this robust, explicit definition of that emberism. I find it quite helpful for understanding ember’s conventions that I’ve often looked past. I’ve bookmarked and circulated it for my team.

So by formalizing with these categories we have the benefit of working in a coherent system that provides inherent validation for how data flows throughout the application?

It strikes me how similar this is to ownership in Rust. While the language and other aspects differ, the problem is similar: who owns said data and what rules govern reading and writing it?

1 Like

Yes, two-way data binding also allows loops and is tricky for that reason. The main difference is that in two-way data binding you are deliberately creating a loop. Here I was mostly talking about accidental loops.

2 Likes

I used to have this type of conversation a lot. I would explain why it wasn’t a good idea, but people kept usig them. Eventually I changed my approach to: when would it make sense to use an observer? My take:

> Observers are the right tool when syncing data that is being shared with another application. The usual examples are apps that do data visualization or maps rendering.

The reason I took this approach was that a few times I would hear something along the lines of –its part of the framework and its public API, so it must be good for something and I’m using it because it makes my app work– sigh.

So, I tried to show them the best examples of true nails for the Observer hammer.

2 Likes

I think one place where I have broke 6 is when components are reused between page transitions. This should likely be fixed by @tracked I imagine, but without this a component that is re-used will persist any state/not reset on the next page.

  1. Data changes may not cause actions.
didInsertElement() {
  this.router.addObserver('currentURL', this, '_doSomething');
}