📝

【メモ: Firestore + Angular】数珠繋ぎの処理を直前のデータと比較して以降処理しないようにする。

2024/01/05に公開
  1. 自分が所属するグループ一覧を取得
  2. 取得したグループのメンバーリストを取得
  3. 取得したグループ一覧とメンバーリストをテンプレートで表示しやすいように整える

(Firestoreなどリアルタイムにドキュメントをリッスンしているのを想定しています)

こんなことがしたい時、rxjsを使ってなにも考えずに実装すると以下のようなコードになるんじゃないかなと思います。
(combineLatestでグループ一覧をループ取得している事自体実装として良くないですが、プロジェクトを運用していく中でそうならざるを得ない状況が生まれる意図や、処理が複雑になっているのを表現する目的の為ご容赦ください。

interface Group {
  id: string
  name: string
  ...
}

interface Member {
  id: string
  groupId: string
  name: string
  status: string
  ...
}
const merge = (groups: Group[], members: Member[]) => {
  const membersMap = members.reduce((acc, member) => {
    if (!(member.groupId in acc) || !Array.isArray(acc[member.groupId])) {
      acc[member.groupId] = []
    }
    acc[member.groupId].push(member)
    return acc
  }, {})
  return groups.map((group) => {
    return {
      ...group,
      members: membersMap[group.id] || []
    }
  })
}

@Component({ ... })
export class MembersComponent {
  authSv = inject(AuthService)
  groupSv = inject(GroupsService)
  membersSv = inject(MembersService)
  
  groups$ = this.authSv.getUid().pipe(mergeMap((uid) => this.groupSv.get(uid)))

  members$ = this.groups$.pipe(
    mergeMap((groups) => combineLatest(groups.map((group) => this.membersSv.get(group.id))).pipe(
      map((members) => ({ groups, members }))
      )
    )
  )
  groupMembers$ = this.members$.pipe(map(({ groups, members }) => merge(groups, members)))
  
  // ----------- もしくは
  
  groups$ = this.authSv.getUid().pipe(mergeMap((uid) => this.groupSv.get(uid)))

  members$ = this.groups$.pipe(
    mergeMap((groups) => combineLatest(groups.map((group) => this.membersSv.get(group.id))))
  )
  groupMembers$ = combineLatest([this.groups$, this.members$]).pipe(map(([groups, members]) => merge(groups, members)))
}

ただこれだと、グループもしくはメンバーの一部情報が変わるたびに情報の再取得と成型を行ってしまいます。
それを防ぐ為に直前の情報と新しい情報を比較しfilterにかけます。その場合の処理が以下です。

const isSameGroup = (before: Group, after: Group) => {
  return before.id === after.id && before.name === after.name
}

const isSameGroups = (before: Group[], after: Group[]) => {
  return before.length === after.length && before.every((b) => after.some((a) => isSameGroup(b, a)))
}

const isSameMember = (before: Member, after: Member) => {
  return before.id === after.id && before.name === after.name && before.status === after.status
}

const isSameMembers = (before: Member[], after: Member[]) => {
  return before.length === after.length && before.every((b) => after.some((a) => isSameMember(b, a)))
}
@Component({ ... })
export class ClientsComponent {
  authSv = inject(AuthService)
  groupSv = inject(GroupsService)
  membersSv = inject(MembersService)
  
  groups$ = this.authSv.getUid().pipe(mergeMap((uid) => this.groupSv.get(uid)))

  members$ = this.groups$.pipe(
    pairwise(),
    filter(([before, after]) => !isSameGroups(before, after)),
    map(([, after]) => after),
    mergeMap((groups) => combineLatest(groups.map((group) => this.membersSv.get(group.id))))
  )
  
  currentMembers$ = this.members$.pipe(
    pairwise(),
    filter(([before, after]) => !isSameMembers(before, after)),
    map(([, after]) => after),
  )
  
  groupMembers$ = combineLatest([this.groups$, this.currentMembers$]).pipe(map(([groups, members]) => merge(groups, members)))
       
}

1. pairwiseで直前のデータと最新のデータを[previous, current]で取得できるようにする
2. filterで直前と最新の情報を比較し以後の処理に流れるかを判断させる
3. [previous, current]の形を元の形に戻す

といった処理をしています。
こんな感じにすると、任意のFieldが更新された時のみ再取得されるようにできるんじゃないかなと思います。
ただ、個人的にはかなり可読性が下がり且つ「なんか余計な事して処理重くしてるんじゃないか?」感が出てしまいちょっと使いづらいです。

そこでSignalsを利用してもう少し改善しようかと思います。それが以下です。

groups$ = this.authSv.getUid().pipe(mergeMap((uid) => this.groupSv.get(uid)))
groups = computed(() => {
    return toSignal(this.groups$, { initialValue: [] })()
}, { equal: isSameGroups })

members$ = toObservable(this.groups).pipe(
    mergeMap((groups) => combineLatest(groups.map((group) => this.membersSv.get(group.id))))
)
members = computed(() => {
    return toSignal(this.members$, { initialValue: [] })()
}, { equal: isSameMembers })
  
groupMembers = computed(() => {
    return merge(this.groups(), this.members())
})

こんな感じです。
observableからsignalにsignalをobservableに変換する関数があるので

import { toSignal, toObservable } from '@angular/core/rxjs-interop'

これを利用し、且つsignalの等値判定処理をカスタマイズしてあげることで、無理矢理感もなく指定したFieldの変更がなかった場合、以降の取得or成型処理を止めてしまい変更なしという扱いにすることで、現在の表示内容のままを維持するといった書き方が可能になります。

実際の処理速度などは未計測です

Discussion