ServerTimeStampをyyyy/mm/dd hh:mmの形に変換し時刻を表示する

5 min read読了の目安(約5300字

はじめに

前回の記事で扱ったチャットアプリに投稿時刻を追加します。

手順は以下の通りです。

  • メッセージ送信時に serverTimeStamp で Firebase のサーバー時刻を取得する
  • 変数 Messages に Firestore からデータを取得する
  • オブジェクト messages に toDate() メソッドを用いて serverTimeStamp を変換し、プロパティを追加する。
  • 画面上にレンダリングする

前提:追加前のコード

前回の記事の「変更後のコード」トピックスに記載しています。

https://zenn.dev/danbo/articles/77aea0b5696f70

投稿時刻表示の処理

メッセージ送信時に serverTimeStamp で Firebase のサーバー時刻を取得する

メッセージフォームの送信時に
firebase.firestore.FieldValue.serverTimestamp()を用いてサーバー時刻を messages コレクションに追加します。
new Date()ではユーザーのデバイスの日時が取得されるため、タイムゾーンが異なる場合に不都合が生じることから、Firebase のサーバー時刻を投稿時刻として取得しています。

Room
export const Room = () => {
    const [value, setValue] = useState("");
    const timestamp = firebase.firestore.FieldValue.serverTimestamp();

    const handleSubmit = (e) => {
        e.preventDefault();
        firebase.firestore().collection("messages").add({
        content: value,
        user: user.displayName,
        time: timestamp,
        avatar: user.photoURL,
        });
        setValue("");
    };

...

return (
  <form onSubmit={handleSubmit}>
    <input
      type="text"
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
    <button type="submit">送信</button>
  </form>
  );
};

これで firestore の messages コレクションに time として投稿時刻が追加されました。

変数 Messages に Firestore からデータを取得する

次は Firestore の messages コレクションからonSnapshot()メソッドでリアルタイムにデータを取得し、変数 messages を定義します。

Room
useEffect(() => {
    firebase
      .firestore()
      .collection("messages")
      .orderBy("time")
      .onSnapshot((snapshot) => {
        let messages = snapshot.docs.map((doc) => {
          return doc.data();
        });
        setMessages(messages);
  }, []);

しかし、この time をコンソールに表示させると、
time: t {seconds: 1617202196, nanoseconds: 51000000}
という形になっており、このままでは表示できないため JS Date 型に変換する必要があります。

オブジェクト messages に toDate() メソッドを用いて serverTimeStamp を変換し、プロパティを追加する。

onSnapShot()内に変換処理を追加します。
map 関数を用いてtoDate()メソッドで serverTimeStamp を year,month,date,hour,min としてそれぞれ変換します。
変換したものはプロパティとしてオブジェクトに追加します。
その際、月日時分を2桁表示にしたいため、頭に 0 をつけて2桁にするよう処理を行っています。

Room
messages = messages.map((message) => {
  message.year = message.time.toDate().getFullYear();
  message.month = ("0" + (message.time.toDate().getMonth() + 1)).slice(-2);
  message.date = ("0" + message.time.toDate().getDate()).slice(-2);
  message.hour = ("0" + message.time.toDate().getHours()).slice(-2);
  message.min = ("0" + message.time.toDate().getMinutes()).slice(-2);
  return message;
});

この状態で messages をコンソール上に表示すると、

となり、変換された数値が格納されています。

しかし、このままだと送信時に、
TypeError: Cannot read property 'toDate' of null
というエラーが出てしまいます。
これはメッセージ送信時に serverTimeStamp の作成をしますが、その作成途中で firestore のデータの取得が起こっているため、timestamp が null となっており、toDate()が使えなくなっています。
ですので、timestamp が null でも変換ができるように処理を追加します。

DocumentSnapshot の data メソッドには SnapShotOptions というオプションを指定することができます。

https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot?hl=ja
https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotOptions?hl=ja
serverTimestamps: "estimate" | "previous" | "none"とあるように、3種類の値を指定できます。
説明
estimate 保留中(serverTimeStamp が null)の時は見積もり時刻を返してくれる。 timestamp を利用できるようになると、その値に変更する
previous 保留中の timestamp は無視し、更新前の値を返す
省略 または none timestamp を利用できるようになるまでは null で返す

今回はserverTimestamps: "estimate"を指定します。

Room
useEffect(() => {
    firebase
      .firestore()
      .collection("messages")
      .orderBy("time")
      .onSnapshot((snapshot) => {
        let messages = snapshot.docs.map((doc) => {
          return doc.data({ serverTimestamps: "estimate" });
        });
        messages = messages.map((message) => {
          message.year = message.time.toDate().getFullYear();
          message.month = ("0" + (message.time.toDate().getMonth() + 1)).slice(-2);
          message.date = ("0" + message.time.toDate().getDate()).slice(-2);
          message.hour = ("0" + message.time.toDate().getHours()).slice(-2);
          message.min = ("0" + message.time.toDate().getMinutes()).slice(-2);
          return message;
        });
        setMessages(messages);
      });
  }, []);

画面上にレンダリングする

Item コンポーネントに props を渡し、
{year}/{month}/{date} {hour}:{min}
の形でレンダリングします。

Room
export const Room = () => {

...

return (
    <>
      <h1>Room</h1>
      <ul>
        {messages &&
          messages.map((message, index) => {
            return (
              <Item
                key={index}
                user={message.user}
                content={message.content}
                avatar={message.avatar}
                year={message.year}
                month={message.month}
                date={message.date}
                hour={message.hour}
                min={message.min}
              />
            );
          })}
      </ul>

    ...

    </>
  );
};
Item
export const Item = ({user, content, avatar, year, month, date, min, hour}) => {
  return (
    <li>
      <img style={{ width: "100px" }} src={avatar} alt="" />
      <br />
      {user} : {content}
      <br />
      {year}/{month}/{date} {hour}:{min}
    </li>
  );
};

結果

最終的にこのように表示ができるようになりました。
送信時にもエラーは起きません。