Firebase Angular2 - how to query and render lists

2020-04-12 08:16发布

问题:

In my application I have the following firebase posts structure:

posts: {
    text: ".."
    meta: {..}
    user: {
        id: "user1",
        username: ".."
    }
}

I'm using angularfire2. So, to get all posts posted by specific user, I run the following query:

this.userPosts$ = this.af.database.list('/posts', {
    query: {
      orderByChild: 'user/id',
      equalTo: userId
    }
  }).map( (posts) => {
    return posts.map(post => Post.unpack(post));
  }).publishReplay(1).refCount();

I've heard that it's not very efficient, since every time something changes (even in a single post), the whole list is reloaded and, thus, re-rendered. And since I have likes and dislikes in my app, this is gonna happen pretty often. Basically every time someone likes a post, the whole list is reloaded for every user, if I'm not mistaken.

I know that there is a way to maintain a local copy of the list while listening to firebase events: "child_added", "child_removed" and "child_changed". However, if I attach these events to the queried list, they are going to fire each time something changes in the original posts list as a whole.

So, I am thinking what approach to take here. I might duplicate posts data into "user_posts" list in firebase if it is the only good choice. However, with this approach I feel that querying becomes almost useless and I lose flexibility. I started to use firebase just about a week ago, so I might still have some misunderstandings.

回答1:

When looking into this question, I discovered an issue with the FirebaseListObservable implementation when preserveSnapshot was false - the default behaviour. Each time the observable emitted an array of unwrapped snapshots, the array elements were different instances - even if the elements themselves had not changed. And this resulted in much less efficient change detection.

A fix has been merged and will be included in the next release of AngularFire2 (that is, the release that follows 2.0.0-beta.7) - which should be soon.

With the fix, if Angular's OnPush change detection is used, AngularFire2's FirebaseListObservable is very fast and efficient regarding DOM updates.

I usually split the components into container and presentational components, with the container component holding the observable and with the presentational component using OnPush change detection.

The container component would look something like this:

import { Component } from "@angular/core";
import { AngularFire, FirebaseListObservable } from "angularfire2";

@Component({
  selector: "items-container",
  template: `<items-component [items]="items | async"></items-component>`
})
export class ItemsContainer {

  private items: FirebaseListObservable<any>;

  constructor(private angularFire: AngularFire) {
    this.items = angularFire.database.list("some/fast-changing/data", {
      query: {
        limitToLast: 1000
      }
    });
  }
}

And the presentational component would look something like this:

import { ChangeDetectionStrategy, Component, Input } from "@angular/core";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "items-component",
  template: `
    <ul>
      <li *ngFor="let item of items">{{ item | json }}</li>
    </ul>
  `
})
export class ItemsComponent {
  @Input() items: any[];
}

With this arrangement of components, Angular's OnPush change detection will ensure that only the elements that have actually changed will effect DOM updates. So even long, scrolling lists will be efficient and fast.

Another tip: a FirebaseListObserable that uses a limit query will sometimes emit two values when the underlying data changes. For example, when a new item is added to the database, a list that queries the 10 most recent items (using limitToLast: 10) will emit an array with the least recent item removed and will then immediately emit another with the most recent item added. If you don't want the first of those two arrays to be emitted, you can use the auditTime operator:

import "rxjs/add/operator/auditTime";

this.items = angularFire.database.list("some/fast-changing/data", {
  query: {
    limitToLast: 10
  }
})
.auditTime(0);