Gatsby でのタイムゾーンを考慮した日時の取り扱い方

目次

はじめに

Gatsby の GraphQL で formatString を用いて時刻を扱うとユーザーのタイムゾーンによっては日時が正しく表示されません。タイムゾーンを扱いながら正しい日付を表示する方法を解説します。
(Gatsby は v5 を使用)

背景

当ブログでは、作成日時・更新日時を取り扱っていているのですが、タイムゾーンの考慮に苦労しました。
下記のようなスキーマを想定してみます。
type BlogPost {
  created: DateTime! # "1960-01-01T00:00+00:00"
}

query Query {
  allBlogPost: [BlogPost!]!
}

Gatsby の GraphQL で時刻を扱う

まず、Gatsby の GraphQL で DateTime を扱う際、 formatString という便利関数が使用できることを知り、使ってみることにしました。
GraphQL Query Options | Gatsby
https://www.gatsbyjs.com/docs/graphql-reference/
query Query {
  allBlogPost {
    a: created
    b: created(formatString: "YYYY-MM-DD HH:mm:ss")
  }
}
この結果、どうなると思いますか?
なんと、このようになります。
{
  "a": "1960-01-01T00:00+09:00",
  "b": "1959-12-31 15:00:00"
}
formatString は、ビルド環境やユーザー環境にかかわらず、 UTC の時間を返します。また、タイムゾーンの指定はできないようでした。
一見便利そうですが、 UTC でフォーマットされていて理解が得られる場合以外には使えませんね。。

タイムゾーン付きで取得し、JS でフォーマットする

そこで、GraphQL にはデータの取得のみ任せて、JS でフォーマットすることにしました。
ライブラリとして date-fns と date-fns-tz を使います。 (それぞれ 2.29.3, 1.3.7 で確認)
date-fns/date-fns: ⏳ Modern JavaScript date utility library ⌛️
https://github.com/date-fns/date-fns
date-fns-tz - npm
https://www.npmjs.com/package/date-fns-tz
このような便利関数を作ってみました。
import { format as formatFn, isValid, parseISO } from "date-fns";

export const formatDateTime = (value: string, format: string): string => {
  const parsedDate = parseISO(value);

  if (!isValid(parsedDate)) return "";

  return formatFn(parsedDate, format);
};
ISO 形式の日時を受け取り、ローカルのタイムゾーンで format して返します。
一見これで動くのですが、ビルド環境とユーザーの閲覧環境でタイムゾーンが異なった場合、静的 HTML と整合性が取れずに hydration error が発生してしまうという大きな問題があります。パフォーマンスに影響が出てしまいます。

タイムゾーン付きで取得し、JS で日本時間にフォーマットする

当ブログは、日本に住んでいるユーザーを対象にしたブログであるため、JST で固定することにしました。
import { isValid } from "date-fns";
import { format as formatFn, utcToZonedTime } from "date-fns-tz";
import { ja } from "date-fns/locale";

const timeZone = "Asia/Tokyo";

export const formatDateTime = (value: string, format: string): string => {
  const parsedDate = utcToZonedTime(value, timeZone);

  if (!isValid(parsedDate)) return "";

  return formatFn(parsedDate, format, { locale: ja, timeZone });
};
以上で、正しい日本時間の日時を表示することができました。

(おまけ) タイムゾーンを変更して単体テスト

単体テストでタイムゾーンを固定したい場合、 process.env.TZ を変更すれば良いのではと考えてしまいがちなのですが、 node のプロセス実行前にタイムゾーンを設定しなければ動作しないようです。
is it a bug? setting up process.env.TZ depends on order of execution. · Issue #3449 · nodejs/node
https://github.com/nodejs/node/issues/3449
そのため、タスクランナーで環境変数 TZ を設定した上で jest を呼び出す方法を取る必要があります。
  "scripts": {
    "test": "jest",
    "test:vietnam": "cross-env TZ=\"Asia/Ho_Chi_Minh\" yarn test"
  },
(特定のプラットフォームに依存しないように cross-env を使用しています)

まとめ

タイムゾーンを持った日時を受け取り、もろもろ処理してクライアントの環境に依存せずに正しい日時を表示することが出来ました。タイムゾーンを持った日時を受け取ったら、何かしらの処理が必要なことを忘れないように気をつけます(自戒)

シェア

Twitter
Facebook
はてブ
LinkedIn
LINE
Pocket