Skip to content

Deprecate Observers - #1115

Merged
NullVoxPopuli merged 10 commits into
emberjs:mainfrom
wagenet:deprecate-observable
Nov 2, 2026
Merged

NullVoxPopuli merged 10 commits into
emberjs:mainfrom
wagenet:deprecate-observable

Conversation

@wagenet

@wagenet wagenet commented Jun 16, 2025

Copy link
Copy Markdown
Member

Propose Deprecating Observers

Rendered

Summary

This pull request is proposing a new RFC.

To succeed, it will need to pass into the Exploring Stage, followed by the Accepted Stage.

A Proposed or Exploring RFC may also move to the Closed Stage if it is withdrawn by the author or if it is rejected by the Ember team. This requires an "FCP to Close" period.

An FCP is required before merging this PR to advance to Accepted.

Upon merging this PR, automation will open a draft PR for this RFC to move to the Ready for Released Stage.

Exploring Stage Description

This stage is entered when the Ember team believes the concept described in the RFC should be pursued, but the RFC may still need some more work, discussion, answers to open questions, and/or a champion before it can move to the next stage.

An RFC is moved into Exploring with consensus of the relevant teams. The relevant team expects to spend time helping to refine the proposal. The RFC remains a PR and will have an Exploring label applied.

An Exploring RFC that is successfully completed can move to Accepted with an FCP is required as in the existing process. It may also be moved to Closed with an FCP.

Accepted Stage Description

To move into the "accepted stage" the RFC must have complete prose and have successfully passed through an "FCP to Accept" period in which the community has weighed in and consensus has been achieved on the direction. The relevant teams believe that the proposal is well-specified and ready for implementation. The RFC has a champion within one of the relevant teams.

If there are unanswered questions, we have outlined them and expect that they will be answered before Ready for Release.

When the RFC is accepted, the PR will be merged, and automation will open a new PR to move the RFC to the Ready for Release stage. That PR should be used to track implementation progress and gain consensus to move to the next stage.

Checklist to move to Exploring

  • The team believes the concepts described in the RFC should be pursued.
  • The label S-Proposed is removed from the PR and the label S-Exploring is added.
  • The Ember team is willing to work on the proposal to get it to Accepted

Checklist to move to Accepted

  • This PR has had the Final Comment Period label has been added to start the FCP
  • The RFC is announced in #news-and-announcements in the Ember Discord.
  • The RFC has complete prose, is well-specified and ready for implementation.
    • All sections of the RFC are filled out.
    • Any unanswered questions are outlined and expected to be answered before Ready for Release.
    • "How we teach this?" is sufficiently filled out.
  • The RFC has a champion within one of the relevant teams.
  • The RFC has consensus after the FCP period.

Comment thread text/1115-deprecate-observable.md Resolved
@ef4

ef4 commented Aug 29, 2025

Copy link
Copy Markdown
Contributor

RFC review discussion notes:

Comment thread text/1115-deprecate-observable.md Resolved
@ganapathyHf

Copy link
Copy Markdown

Hi @wagenet . We have a use case where external services sent a realtime notification which our app listens via observer and runs a callback. I think this is a very standard use case. Would be glad to know how to migrate such use cases where the trigger happens outside the app

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

@ganapathyHf can you share more about your use case, and maybe a specific code example?

For truely evented behavior, we have
https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
And many other ways to tell another system something else.

The easiest (and most controlled) is passing a function to an external library

@ganapathyHf

ganapathyHf commented Oct 28, 2026 •

Copy link
Copy Markdown

@NullVoxPopuli Sure

Here is an example flow of how we handle realtime notifications

WebSocket message
       ↓
Realtime service (inside app/services)
       ↓
`notification` state = socket event with data
       ↓
Observers in feature-specific services/controllers
       ↓
Filter the notification
       ↓
Refresh the relevant data / display toast / show progress etc

The realtime service owns the WebSocket connection. When a socket event is received, it normalizes the payload and assigns it to a notification property:

realTimeService.notification = notificationData

Different parts of the application observe this property and react when it changes.

For example, the Knowledge Base area observes realtime notifications because its metadata can be changed by another user:

realTimeService: service('real-time'),

onRealtimeNotification: on(
  'init',
  observer('realTimeService.notification', function() {
    const notification = this.get('realTimeService.notification');

    if (notification is related to a particular feature) {
      fetch the latest feature metadata;
    }
  })
)

The benefit was the web socket implementation remains centralized in the realtime service, while individual features decide which notifications are relevant to them.

Our app has a mix of both classic and Octane patterns. So in case of Octane, currently we follow a workaround where we register the listener as computed decorator

    @computed('realTimeService.notification')
    get onRealTimeNotification() {
        // fetch the relevant data
     }

And to invoke this, we mention them in the corresponding template

{{this.onRealTimeNotification}}

As I mentioned this is a workaround and very implicit.

Hope the examples are clear. I would like to know what is the preferred approach to migrate off from both of these patterns?

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

thanks for the examples!

Here is the minimal change way how I'd do the notification thing across the whole app, since:

i see:

  • upon receiving a notification
    • you fetch data in various places across the app
  • a realTimeService is a instance of something maintained by the ember app
// app/services/real-time-service.js
export default class RealTimeService extends Service {
  @tracked lastNotification;
}

// passing the real time service somewhere
// you didn't show this part in your examples, but I assume it's something like this.
// there are many ways to pass data/objects/etc to external things
class X {
  @service realTimeService;

  setup() {
    // MyExternalThingNotFromEmber 
    // can still do realTimeService.lastNotification = newNotification
    this.#external = new MyExternalThingNotFromEmber(this.realTimeService);
  }
}

and then for your in app-usage of handling changes to lastNotification -- you still want to use data derivation:

class X {
  @service realTimeService;

  get isNotificationRelevant() {
    return this.realTimeService.lastNotification.includes('something');
  }

  // it's important that this is cached, else we'll unneededly 
  // trigger multiple fetches when tracked data has not changed
  @cached 
  get request() {
    if (!this.isNotificationRelevant) return;
    
    return fetch(...)
  }
  
  // getPromiseState here can be from warp-drive, or reactiveweb
  // it's a good general-purpose utility for telling ember about promises
  // - https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html
  // - https://warp-drive.io/api/@warp-drive/core/reactive/functions/getPromiseState#getpromisestate
  // 
  // See Also: 
  // - https://github.com/emberjs/rfcs/pull/1060
  get state() {
    return getPromiseState(this.request);
  }
}

And for your second example, the most direct way to do that would be to:

class X {
  @service realTimeService;

  doSomething = () => {
    let notification = this.realTimeService.lastNotification
  }

  <template>
    {{ (this.doSomething) }}
    ...
  </template>
}

This has caveats though, as this is how you do an "effect" in ember, and like with all effects in all frameworks, they have the same caveats -- in that if not careful, you can cause infinite loops (which are hard to detect, due to how the timing of your effects work).

That said, this is a common enough pattern, where I made a utility to try to make this as safe as possible here:

The code is very simple (most of this is just documenting the pattern):


Now, something that I think could be a fun little plain JS, don't worry about what ember is doing way of handling your observer case is to make your own event system -- which is actually very few lines -- for example:

at the consumer side:

// ..
@service realTimeService;

this.realTimeService.onNotification(() => { /*....*/ })


// in real-time-service.js
export default class RealTimeService extends Service {
  /**
   * no one is allowed to set this directly
   * Using tracked like this requires ember 7.3+
   */
  #lastNotification = tracked(null);
  #callbacks = [];
 
  onNotification = (callback) => {
    this.#callbacks.push(callback);
  }

  // consumers now use this
  setNotification = (newNotification) => {
    this.#lastNotification.value = newNotification;
    this.#flushSubscribers();
  }
  
  #flushSubscribers() {
    let lastNotification = this.#lastNotification.value;

    for (let callback of this.#callbacks) {
      callback(lastNotification);
    }
  }
}

@ganapathyHf

Copy link
Copy Markdown

@NullVoxPopuli I am going through the examples. Will let you know if i have any doubts. Wanted to mention that we are currently migrating from 3.28 to v4 so i think usage of template tags might not work. What about other suggestions like getPromiseState etc?

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

template tag works on ember 3.28 via https://github.com/ember-cli/ember-template-imports

  • you'll want to make sure eslint-plugin-ember and ember-template-lint are fully up to date, probably
  • reactiveweb tests against 3.28

since you're on 3.28, going to 4, the only thing from my examples that won't work is the tracked(null) -- but you're already used to decorator form anyway (and likely every property not being private)

@NullVoxPopuli
NullVoxPopuli merged commit f72bf57 into emberjs:main Nov 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Final Comment Period S-Exploring In the Exploring RFC Stage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants