ember.jsember-octane

Invalid syntax on EmberJS Octane observers


I'm trying to use Ember observers with EmberJS Octane latest version (4.1.0), but it does not seem to work.

Here is what I'm trying to achieve :

export default class SummaryService extends Service {
  @service store;
  @service authentication;

  @reads('authentication.userId') userId;

  @tracked weekPosts;
  @tracked monthPosts;

  @observer('userId')
  loadPosts() {
    this._loadPosts(this.userId);
  }

  _loadPosts(userId) {
    this.store.query('posts', { filter: { 'current-week': 1, 'user-id': userId } })
      .then((posts) => {
        this.set('weekPosts', posts);
      });
    this.store.query('posts', { filter: { 'current-month': 1, 'user-id': userId } })
      .then((posts) => {
        this.set('monthPosts', posts);
      });
  }
}

=> The syntax is invalid.

I also tried :

@observer('userId', function() {
  this._loadPosts();
});

=> The observer is indeed called, but this is undefined.

I also tried :

  init() {
    super.init(...arguments);
    this.addObserver('currentUserId', this, '_loadPosts');
  }

=> But this one does not call any method (even with inline method definition).

Finally, my last attempt was to use @computed properties for weekPosts and monthPosts instead, like this :

export default class SummaryService extends Service {
  /* ... */

  @computed('userId')
  get weekPosts() {
    return this.store.query('posts', { filter: { 'current-week': 1 } })
      .then((posts) => { return posts; });
  }
}

=> But it always returns a Promise, so I can't call .reduce on it from a computed property used by a Component :

export default class SummaryComponent extends Component {
  @computed('weekPosts')
  get weekPostsViewsCount() {
    return this.weekPosts.reduce((sum, post) => { return sum + post.viewCount });
  }
}

I finally got something working pretty ugly using an ArrayProxy.extend(PromiseProxyMixin) returned by the weekPosts computed property, but I'm definitely not happy with this for the following reasons :

  1. So much code for such a simple thing
  2. Everything (component, template) which uses the weekPosts has to make sure the promise is fulfilled before working with it
  3. The promise is an implementation detail of the service and should not be visible in any way out of it

Thanks !


Solution

  • Observers won't work for what you want to do -- since it looks like you want to reactively re-fetch data (using ember-data) based on when userId changes, I have a library suggestion:

    With this library, we can replace most of your service with this:

    import { query } from 'ember-data-resources';
    
    export default class SummaryService extends Service {
      @service authentication;
    
      @reads('authentication.userId') userId;
    
      _weekPosts = query(this, 'posts', () => ({
        filter: { 'current-week': 1, 'user-id': this.userId 
      }));
    
      _monthPosts = query(this, 'posts', () => ({
        filter: { 'current-month': 1, 'user-id': this.userId 
      }));
    
      get weekPosts() {
        return this._weekPosts.records ?? [];
      }
    
      get monthPosts() {
        return this._monthPosts.records ?? [];
      }
    
      get isLoading() {
        return this._weekPosts.isLoading || this._monthPosts.isLoading;
      }
    
    }
    

    The advantage here is that you also have the ability to manage error/loading/etc states.

    This uses a technique / pattern called "Derived state", where instead of performing actions, or reacting to changes, or interacting withe lifecycles, you instead define how data is derived from other data.

    In this case, we have known data, the userId, and we want to derive queries, using query from ember-data-resources, also uses derived state to provide the following api:

    Which then allows you to define other getters which derive data, weekPosts, isLoading, etc.

    Derived state is much easier to debug than observer code -- and it's lazy, so if you don't access data/getters/etc, that data is not calculated.