FirebaseのonSnapshotを使ったときに起きた、serverTimestampがnullになる現象について

React+TypeScriptでアプリを開発している時のこと。

「ユーザーがformに入力した内容をリアルタイムに画面にに反映したいな」と思い(チャットの様なものだと思ってください)、調べていると`get()`ではなく、`onSnapshot()`を使えばFirestore(db)に保存されたデータを即時画面に反映できると知り、使ってみた結果即時反映させることには成功したものの、`なぜかserverTimestamp()がnullになる`エラーに直面しました。

その時の対処法の解説になります。

## コードの例

以下は上手く行った時のReactのコード例ですが、他のフレームワークやライブラリでも同じ様な感じだと思います。

```CommentForm.tsx
  useEffect(() => {
    let comments: any = [];
    const unsubscribe: any = db
      .collection("posts")
      .doc(id)
      .collection("comments")
      .orderBy("createdAt", "desc")
      .onSnapshot((snapshots) => {
        snapshots.docChanges().forEach((change) => {
          const data = change.doc.data({ serverTimestamps: "estimate" });
          const changeType = change.type;
          const date = data.createdAt.toDate();

          switch (changeType) {
            case "added":
              comments.push({ ...data, createdAt: date });
              break;
            case "modified":
              const index = comments.findIndex(
                (comment: any) => comment.id === change.doc.id
              );
              comments[index] = comment;
              break;
            case "removed":
              comments = comments.filter(
                (comment: any) => comment.id !== change.doc.id
              );
              break;
            default:
              break;
          }
        });
        setComment(comments);
        unsubscribe();
      });
  }, []);
```

## 解決した方法

解決した方法はかなり簡単でした。。。

```TypeScript
doc.data({ serverTimestamps: "estimate" }).createdAt
```

### 解説
FirebaseでgetやonSnapshotを使ってデータを呼び出した際に、

```TypeScript
snapshots.docs.forEach(doc => {...})
```
の様な感じで、データを取り出すと思います。

この後に、`doc.data()`とすると一つ一つのデータのオブジェクトが取得できます。

しかし、

```TypeScript
doc.data().createdAt
```
とすると、最新(追加した瞬間)のデータがnullになってしまうのですが、

```TypeScript
doc.data({ serverTimestamps: "estimate" }).createdAt
```
とすることで、追加した瞬間のnullの状態のtimestampに対して、timestampを推定して、`確定するまでとりあえず仮のTimestamp`を入れておいてくれます。

Firebaseの[公式ドキュメント](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot#data)を見ていただけたらわかると思いますが、この`doc.data()`の`data()`に関して以下の様な解説がされています。

>ドキュメント内のすべてのフィールドをオブジェクトとして取得します。文書が存在しない場合は 'undefined' を返します。デフォルトでは、まだ最終的な値に設定されていない FieldValue.serverTimestamp() の値は null として返されます。これをオーバーライドするには、オプションオブジェクトを渡す必要があります。

今回のエラーも加味して簡単に言い換えると、、、

`data()`でオプションを何も指定していない場合は、`FieldValue.serverTimestamp()`が`最終的な値を決定するまでnull`を返します。nullが返されたくないなら、`オプションを指定すればなんとかできます。`

といったところでしょうか。

ドキュメントを見てみると、`data()`のオプションは2つある様です。

| オプション | 説明 |
|:-----------|:------------|
| estimate       | 保留中のサーバータイムスタンプはローカルクロックに基づいた推定値を返します。この推定値は最終的な値とは異なり、サーバーの結果が利用可能になると、これらの値が変更されます。        | 
| previous     |保留中のタイムスタンプは無視され、代わりに以前の値を返します。    |
| none       | 省略された場合や 'none' に設定された場合は、サーバの値が利用可能になるまでの間、デフォルトで null が返されます。        | 


こんなところに大ヒントが書いてありましたね😅

## 答えは公式ドキュメントに📃

Firebaseの[公式ドキュメント](https://firebase.google.com/docs)はやはりかなり充実していて、大抵のfirebase関連の問題はドキュメントに書いてあると思いました😅

今後はFirebaseを使うときはわからなかったらドキュメントをしっかり読む、という習慣を身につけようと思いました。

## 参考
[公式ドキュメント](https://firebase.google.com/docs)

[公式ドキュメントのdataのページ](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot#data)

[公式ドキュメントのdataのoptionのページ](https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotOptions)