FirebaseのonChildAddedとは何か？（Firebaseウェブバージョン9）

この記事では、 **Firebase Realtime Databaseウェブバージョン9** （正確にはバージョン9.8.1）で用いられている"onChildAdded"という関数について説明します。

Firebaseでブラウザで動くチャットアプリを作るサンプル教材を学習した際に出てきて、その際にはあまりonChildAddedの意味をわかっていなかったのですが、どうにかこうにか理解しました。

それにしても、検索エンジンで検索をかけても、Firebaseバージョン8時代の古い情報ばかりがヒットしてしまうのなかなか辛いですね。。。

# 前提情報の整理～作ろうとしていたFirebaseウェブアプリの共有～

まずは前提をそろえるためにどのようなチャットアプリを作ろうとしていたかを共有します。

画像. チャットアプリの画面(↓)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573922/1e8fa9df-02c9-28c1-e7c7-a9725b503003.png)

このように、画面に名前と文章を打ち込んだらチャットができるとの仕様のウェブアプリです。
事前に、FirebaseをJavascriptプロジェクトに追加する方法については以下の記事等をご参照。

https://qiita.com/kohashi/items/43ea22f61ade45972881
https://firebase.google.com/docs/web/setup?hl=ja

サンプルコードは以下のようになっています

<details><summary>サンプルコードを見る</summary>

```sample.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Firebase:v9:Chatアプリ</title>
  </head>
  <body>
    <!-- コンテンツ表示画面 -->
    <div>
      <div>名前：<input type="text" id="uname" /></div>
      <div>
        <textarea id="text" cols="30" rows="10"></textarea>
        <button id="send">送信</button>
      </div>
      <div id="output" style="overflow: auto; height: 300px;"></div>
    </div>
    <!--/ コンテンツ表示画面 -->

    <!-- JQuery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <!-- JQuery -->

    <!--** 以下Firebase **-->
    <script type="module">
      // Import the functions you need from the SDKs you need
      import { initializeApp } from "https://www.gstatic.com/firebasejs/9.8.1/firebase-app.js";
      import {
        getDatabase,
        ref,
        push,
        set,
        onChildAdded,
        remove,
        onChildRemoved,
      } from "https://www.gstatic.com/firebasejs/9.8.1/firebase-database.js";
      // TODO: Add SDKs for Firebase products that you want to use
      // https://firebase.google.com/docs/web/setup#available-libraries

      // Your web app's Firebase configuration
      // xxxxxの部分には各自のFirebaseに指示された値を記載して下さい
      const firebaseConfig = {
        apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      };

      // Initialize Firebase
      const app = initializeApp(firebaseConfig);
      const db = getDatabase(app);
      const dbRef = ref(db, "chat");

      //送信イベント
      $("#send").on("click", function () {
        const msg = {
          uname: $("#uname").val(),
          text: $("#text").val(),
        };
        // 唯一のkeyを取得する
        const newPostRef = push(dbRef);
        set(newPostRef, msg);
      });

      //受信イベント
      onChildAdded(dbRef, function (data) {
        const msg = data.val();
        const key = data.key; //ユニークキーの取得
        let h = '<p ';
        h += 'data-id="';
        h += key;
        h += '" >';
        h += msg.uname;
        h += '<br>';
        h += msg.text;
        h += '</p>';
        $("#output").append(h);
      });
    </script>
  </body>
</html>
```

</details>

このチャットウェブアプリでも用いられているonChildAddedについて解説していきます

# onChildAddedとはいつどのように動く関数なのか
## 子データが追加された時だけでなく、「読み込み時」にも動く

### ページが読み込まれたときに、onChildAddedがある行も読み込めば動く

関数名を見れば、第一引数に指定されたデータベース直下に子データが追加されたら動くことはほぼ明らかかと思います。しかし見落とされがちなのだ、ページを読み込んだ時にも動く点です。
ここを私も最初に誤解していました。

Firebase日本語版公式ドキュメントにも、一応onChildAddedの挙動について書いてあり、それを読めば、ページを読み込んだ時にも動くことが分かります。
https://firebase.google.com/docs/database/web/lists-of-data?hl=ja#listen_for_child_events
>child_added アイテムのリストを取得します。また、アイテムのリストへの追加をリッスンします。このイベントは既存の子ごとに 1 回トリガーされます。さらに、指定されたパスに新しい子が追加されると、そのたびに再びトリガーされます。

（私は最初child_addedという風に小文字やら_入っていたりやらだったので、公式ドキュメントの中でonChildAddedに関連する事項が書かれていることを見落としていました）

ちなみに子データに追加されたときなので、孫データが追加されたときには動かないはずです。孫データが追加された場合のイベントをリッスンして処理を走らせたい場合は、onChildChangedやonValue等を用いることになります。

### (応用)関数内で読み込まれれば動く

この節は応用的な内容で少々ややこしいので、まずざっくりonChildAddedを理解したい方は読み飛ばして次の見出しの内容を読んでください。

onChildAddedは次の例のように関数内で呼び出すこともできます。この場合は、その関数が呼び出された後にonChildAddedが作動します。

以下の例はチャットアプリとは別のfirabeseウェブを用いたアプリで、リアルタイムにゲームスコアのランキングを表示するために作った関数です。onValueとonChildAddedを組み合わせています。

```関数内でonChildAddedを呼び出している例.js
onValue(dbRef, function (snapshot) {
        const data = snapshot.val();
        console.log(data); //debug
        const topScoreUserRef = query(
          ref(db, `chat/memo1`),
          orderByChild("score")
        ); //スコアが小さい順のデータベースReferenceを生成する
        console.log(topScoreUserRef); //debug
        // 配列とonChildAddedを用いて、scoreが小きい順の配列を作る
        let arrScoreDesc = []; //scoreが大きい順に格納するローカル変数を宣言
        onChildAdded(topScoreUserRef, function (data) {
          console.log(data.val());//debug
          console.log(data.key);//debug
          const hash = {
            key: data.key,
            uname: data.val().uname,
            score: data.val().score,
          };
          arrScoreDesc.unshift(hash); //unshiftで配列の先頭に追加する
        });
        // ランキングとして出力する
        $("#ranking__rows").html(""); //既存のランキング表を空にする
        let rank = 1; // ランクを表示するためのローカル変数
        arrScoreDesc.forEach(function (ele) {
          console.log(ele); //debug
          $("#ranking__rows").append(`<tr><th>${rank}位</th><td>${ele.key}</td><td>${ele.uname}</td><td>${ele.score}</td></tr>`); //昇順のデータを降順で表示するためにprependを用いる
          rank++;
        });
      });
```

## ページ読み込みや関数内で呼び出された際には、for文のような繰り返し処理を行う

ChildAddなどと書いているので、追加された子データ関係のデータを第2引数内に渡しているのではないかと私は勘違いしていました。しかし、ページを読み込んだ際には、すべての子データのDataSnapshotクラスのオブジェクトを繰り返し処理をしながら渡しています。

上の節の(応用)を読んだ方向けに補足すると、関数内で読み込んだ際もすべての子データのDataSnapShotクラスのオブジェクトを返していました。

これは先ほど引用した[Firebase公式ドキュメント](https://firebase.google.com/docs/database/web/lists-of-data?hl=ja#listen_for_child_events)に以下の通り記載されています。

>このイベントは既存の子ごとに 1 回トリガーされます。さらに、指定されたパスに新しい子が追加されると、そのたびに再びトリガーされます。

百聞は一見に如かず！実際にconsole.log()を使って確認してみましょう。

```sample.htmlの一部書き換え版.js
      onChildAdded(dbRef, function (data) {
        console.log(data); //書き足したdebug用の1行
        const msg = data.val();
        const key = data.key; //ユニークキーの取得
        let h = '<p ';
        h += 'data-id="';
        h += key;
        h += '" >';
        h += msg.uname;
        h += '<br>';
        h += msg.text;
        h += '</p>';
        $("#output").append(h);
      });
```
書き足したうえでページを読み込めば、以下のように子データの数だけのDataSnapShotクラスのオブジェクトが取得できているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573922/035c8375-d5d2-5418-1eb9-43fcab854244.png)

各DataSnapShotの中身を見てみると、各子データのkeyやrefといった情報が記載されています。
また、[val()とのメソッド](https://firebase.google.com/docs/reference/js/database.datasnapshot#datasnapshotval)を用いると、JavaScript値を抽出できます。

onChildAddedのイメージとしては、以下のfor文をもとに理解にするとよいかと思います。
```for文の例
      const arr = ["a", "b", "c", "d"];
      for (let i = 0; i < 4; i++) {
        const num = arr[i];
        console.log(arr[i]);
        console.log(num);
      }
```

for文がarr[0]からarr[3]まで順番に繰り返し処理をするのと同様に、
onChildAddedは、順番(※)にDataSnapShotを処理していくということです。

※この順番は、おそらくデータベースに登録された順かと思いますが、確認できていません。

## 子データが追加された際には、繰り返し処理をせずに追加された子データのみを第2引数に渡す

ややこしいのですが、ページを読み込んだ時ではなく、子データが追加された際には、追加された子データのみを第2引数に渡します。以前から存在していた子データは第2引数に渡しません。

たびたび引用している[Firebase公式ドキュメント](https://firebase.google.com/docs/database/web/lists-of-data?hl=ja#listen_for_child_events)に子データが追加された際の処理について記載されています。

>指定されたパスに新しい子が追加されると、そのたびに再びトリガーされます。リスナーには、新しい子のデータを含んでいるスナップショットが渡されます。

私は最初にドキュメントを読んだ時には、「新しい子のデータを含んでいるスナップショット」という言葉の意味を「新しい子のデータを含んだ、すべての子データのスナップショット」との意味かと考えていました。しかし、実際は「 **新しい子のデータのみを含んだスナップショット** 」との意味でした。

日本語って難しいですね。ちなみに英語版の公式ドキュメントを読んでも"containing"と書かれていました。

以上！

## 関連する事項や記事
onChildAddedは、読み込みと子データの追加イベントをリッスンする関数ですが、似たものにonChildChangedというものがあります。onChildChangedは子データの変更イベントをリッスンする関数で、@rahydynさんの[【Firebase】onChildChangedの使い方と注意点 ](https://qiita.com/rahydyn/items/0438960570b39e50dcea)に詳しいです！

https://qiita.com/rahydyn/items/0438960570b39e50dcea

## 超応用編　onChildAddedのイベントリッスンタイミングとその処理について

onChildAddedのイベントリッスンタイミングとその処理について
・読み込まれたときに、全部の子データを繰り返し第二引数の関数に渡す
・子データが追加されたときに,、追加された子データのみを第二引数に渡す
の二つあります。

扱いずらいなと思って、いろいろ実験したのですが、おそらく以下の3パターンに分けられるようです。
### A ページ読み込み時に、childイベントやvalueイベントをリッスンして実行された関数の中で、実行されたonChildAdded
例としては、
```Aパターン.js
onValue(dbRef, function(snapshot){
   onChildAdded(dbRef, function(data){
      console.log(data.key);
   })
})
```
  など。
この場合、
「onChildAddedが関数内で読み込まれた時に、全部の子データを繰り返し第二引数の関数に渡す」
との動きを取ります。
追加された子データのみを第二引数に渡すとの動きを取ることはそれ以降ありません。
常に「onChildAddedが関数内で読み込まれた時に、全部の子データを繰り返し第二引数の関数に渡す」となります。

### B ページ読み込み時に、childイベントやvalueイベントをリッスンせずに実行された関数の中で、実行されたonChildAdded
これは、いわゆる「通常通り」onChildAddedを使用した場合と、ページ読み込み時点でブロック内にあったとしても実行される場合です。
例としては、以下など。
```Bパターン.js
const bool = true;
if(bool){
   onChildAdded(dbRef, function(data){
      console.log(data.key);
   })
}
```
### C ページ読み込み時に実行されなかったonChildAdded
例えば、
```Cパターン.js
function aaa(){
   onChildAdded(dbRef, function(data){
      console.log(data.key);
   })
}

$("#send").on("click", function(){
   aaa();
})
```
この場合、
「onChildAddedが関数内で読み込まれた時に、全部の子データを繰り返し第二引数の関数に渡す」
との動きを取ります。
追加された子データのみを第二引数に渡すとの動きを取ることはありません。Aパターンと同じような動きを取るはずです