Page Visibility APIでFirebaseのRealtime DatabaseのListenerを自動的に取り外す方法

はいさい、ちゅらデータぬオースティンやいびーん！

# 概要
FirebaseのPWAでonValueなどのListenerを使っている時に、ユーザーが画面を見ていない時にそのListenerを取り外す方法を紹介します。

### なぜ取り外すべきか？
PWAでは、アプリのように動くことを想定して作ります。そして、基本的に携帯・スマホをベースに考えてコードを書いていくものなのですが、スマホアプリ開発で破ってはならぬ鉄則があります。

<strong>バッテリー使用を抑えろ。</strong>

という鉄則です。
そこで、スマホアプリだと、ページが見えていない時はService Worker以外、何もしないようにしたいですね。

## FirebaseのRealtime Database - onValueのListenerについて
FirebaseのJavaScript SDKには、Realtime Databaseのとあるところのデータ変更を全てリアルタイムで監視するonValueという関数があります。以下のような使い方をします。
```javascript
import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase() // initializeAppで作ったappをここに入れることを勧めます。
const chatRef = ref(db, "/chat"); // ユーザーのチャットルームなどのメッセージが入っている場所

onValue(chatRef, (allMessages) => {
  const data = allMessages.val();
  updateDOM(data); // DOMを更新する関数
}, (error) => console.error(error));
```
非常に便利な関数ですが、気にするべきことが多々あります。主に以下の3点。
- 入ってくるデータが大量になり、アプリのメモリ使用が膨大になりうる
- 常にchatRefの場所を監視しているので、バッテリー使用が大
- スマホのSuspendと相性が悪く、そのままにすると、Suspendから戻ったページ・アプリがDBから情報を取得しなくなることがある。

## onValueのListenerを取り外す方法
本題の解決に入る前に、onValueのListenerを外す方法をあらかじめ紹介しておきたいです。
以下のように、onValueが返す関数を定数・変数の保存しておけば、Listenerを取り外すことができます。
```javascript
import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase() // initializeAppで作ったappをここに入れることを勧めます。
const chatRef = ref(db, "/chat"); // ユーザーのチャットルームなどのメッセージが入っている場所

const disconnect = onValue(chatRef, (allMessages) => {
  const data = allMessages.val();
  updateDOM(data); // DOMを更新する関数
}, (error) => console.error(error));

disconnect(); // onValueのListenerが止まり、なくなる
```

# 解決法：Page Visibility API
上記の問題を解決するために、<a href="https://developer.mozilla.org/ja/docs/Web/API/Page_Visibility_API">Page Visibility API</a>を使用することができます。
以下のようにdocumentにEventListenerを追加します。

```javascript
document.addEventListener("visibilitychange", () => {
  const visibilityState = document.visibilityState; // "hidden" | "visible";
});
```

onValueと一緒に使えば、以下のような仕組みになります。
```javascript
import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase() // initializeAppで作ったappをここに入れることを勧めます。
const chatRef = ref(db, "/chat"); // ユーザーのチャットルームなどのメッセージが入っている場所

let disconnect = () => {} // ここに最新のonValueのListenerを取り外す関数を入れておく
const establishDatabaseConnection = () => {
  disconnect = onValue(chatRef, (allMessages) => {
    const data = allMessages.val();
    updateDOM(data); // DOMを更新する関数
  }, (error) => console.error(error));
}

establishDatabaseConnection(); // 初回

document.addEventListener("visibilitychange", () => {
  const visibilityState = document.visibilityState; // "hidden" | "visible";
  if (visibilityState = "visible") establishDatabaseConnection(); //  ユーザーが戻ったときにListenerを付け直す
  if (visibilityState = "hidden") disconnect(); // onValueのListenerが止まり、なくなる
});
```
これで、ユーザーが見ていない時にむやみにバッテリーを使わないようにできます！

## ボーナス：Web Component APIとの円滑の組み込み方
Web ComponentsでFirebaseアプリを作るのはおすすめです！相性抜群です。
上記の仕組みを綺麗にWeb Componentのclassでまとめることができます。
```javascript
import { getDatabase, ref, onValue } from "firebase/database";

class ChatBox extends HTMLElement {
  #disconnect;
  #chatRef;

  constructor() {
    super();
    this.#disconnect = () => {};
    const db = getDatabase();
    this.#chatRef = ref(db, "/chat");
  }

  connectedCallback() {
    this.#establishDatabaseConnection(); // Web ComponentがDOMに入った時に一度実行される
    document.addEventListener("visibilitychange", this.#handleVisibilityChange);
  }

  disconnectedCallback() {
    this.#disconnect(); // Web ComponentがDOMから消えた時に実行される
    document.removeEventListener("visibilitychange", this.#handleVisibilityChange);
  }

  #handleVisibilityChange = () => {
    const visibilityState = document.visibilityState;
    if (visibilityState = "visible") establishDatabaseConnection();
    if (visibilityState = "hidden") disconnect();
  });

  #establishDatabaseConnection() {
    this.#disconnect = onValue(this.#chatRef, (allMessages) => {
      const data = allMessages.val();
      this.#updateDOM(data);
    }, (error) => console.error(error));
  }

  #updateDOM() {...}
}

```
#updateDOMのところに、以前の記事で紹介したLit-htmlを使うことをお勧めします！

https://qiita.com/tronicboy/items/b0008bcf43f3fd46d8c9

# まとめ
以上、Page Visibility APIでonValueの悪い側面を少し減らす方法を紹介しました！

筆者の推測ですが、onValueでListenerをつけて、disconnectで取り外すのにもバッテリー使用が予想されるので、もし頻繁に開けては閉じるようなアプリ（どんなアプリかな？想像がつかん）を開発されているのであれば、このような仕組みは不要なのかもしれません。