Observers in Classic Components

Hi all,

We are using Ember 3.4. In the official Ember documentation, it is mentioned that observers should be used sparingly, mainly because they are already heavily used internally in the framework.

I have some doubts about how observers work internally. My understanding is that observers and computed properties are similar in some ways—they are both triggered when dependent properties change. (Yes, I know that computed properties only recompute when they are consumed.)

The main difference I see is:

  • In computed properties, we don’t update other properties;

  • In observers, we often update other properties or perform side effects.

I assume the internal mechanism for triggering both observers and computed properties is similar.

Could someone explain how observers are implemented under the hood and why they are discouraged, while computed properties are recommended? Understanding this will help us find alternative approaches to replace observers safely in our code.

Any insights would be really helpful.

In observers, we often update other properties or perform side effects.

I think this is really the main thing. Observers should be used very sparingly because they’re almost never necessary and lead to bad practices such as side effects and hard to debug chains of effect. The generally aligned-on convention is to use “derivations” and one way data flow. If components are designed well it should be pretty rare that you’d need to react to state changes with a side-effect. So all that to say I don’t think it’s the underlying implementation that’s relevant in the reasoning, it’s that the community (and really the JavaScript/FE community as a whole) has emphasized one-way data flow patterns and discouraged unnecessary side-effects.

Also I’m not actually familiar with how the internals are implemented these days but I think think that in the new reactivity system (autotracking which is essentially signals based) there actually is more difference between observers and computed properties under the hood.

As far as alternatives and migration strategy:

  • There are bigger fish to fry in all likelihood. I personally would recommend prioritizing Ember upgrades over removing observers. Observers still work as of latest stable IIRC and we’ve taken our sweet time migrating away from observers (slow steady progress).
  • Focus on good component patterns instead of just “an alternative tool that does the same thing”. If you can remove the need for observer patterns at all you’ll be in better shape than if you just switch to a direct alternative (e.g. the did-update helper or modifier).
  • If you want to progressively migrate away from them you can use the aforementioned did-update modifier from ember-render-modifiers or helper from ember-render-helpers (although that may be difficult in your current version of Ember I can’t remember when those were introduced or what compatibility looks like). Those are also generally discouraged (for the same reasons as observers) and were mainly seen as a tool to help slowly migrate away from observers and other lifecycle hooks and associated behaviors.

Thanks for the clarification — that helps. I understand now that the discouragement around observers is mainly about enforcing predictable patterns (derivations and one-way data flow), rather than the underlying implementation.

We’re aligned with Ember’s data-down, actions-up approach, but due to current constraints we can’t upgrade Ember right now. In this context, we have cases where reacting to a specific argument change is necessary. For example, a scroll component accepts multiple arguments, one of which is scrollTo. When scrollTo changes, the component needs to imperatively update the scroll position. With didReceiveAttrs, we can’t reliably tell whether scrollTo itself changed, which is why observers were originally used.

Given these constraints, we’re trying to improve things incrementally without large refactors. We’d really appreciate any suggestions on alternative patterns or approaches that would work better here while staying within older Ember versions.

In the case of things like scroll a modifier is probably a better abstraction. Modifiers can be attached to DOM elements (or components, which forward to their primary DOM element) and are useful for capturing “layout effects” (to use the React terminology).

It appears that you should be able to use ember-modifier 1.x if you don’t have it already, but compatibility might be a little tricky since you’re barely in the supported range. You can also use ember-render-modifiers 1.x which again isn’t ideal (because it doesn’t fix patterns) but can be used as a good stepping stone to more gradually migrate off observers (typically a replacement for an observer would be {{did-insert …}} to fire on initial render and then {{did-update … dependentProp1 dependentProp2 … }}to fire when the dependent props change).

EDIT: to be a little more explicit:

  • in the long run it’s best to move towards DDAU (data down actions up) and one-way data flow as a general architectural pattern
  • you can use ember-render-modifiers as a stepping stone to let you modernize things and avoid observers, but take care not to make it a “bad habit” or a critical part of your code
  • for layout effects like scroll or direct DOM interactions a custom modifier is probably the best abstraction
  • observers are still around! they are (generally) discouraged as a pattern but in the 7 years of development since Ember 3.4 was released they haven’t been removed. They do change a bit in later Ember 3.x. But all that to say it’s not critical to get off them right away. The biggest takeaway is to think carefully about new/refactored code and make good architectural decisions (one way data flow, one way binding, data down actions up, etc) and abstractions (like modifiers) where appropriate.