ReactのSSRで日時表示がクライアントサイドと違うため再レンダーされてしまう


Next.js に限らず React で SSR や SSG をしているときにサーバーサイドとクライアントサイドの仮想DOMに差異が発生するとブラウザーで再レンダーが実行されてしまいます。
とくに Date オブジェクトを使用していると環境によって出力される文字列が変わることがあり、年に一回くらい社内でこの問題に遭遇するので記事にしておきます。

※なお、この問題は Node v13.x 以降のデフォルト状態ではおそらく起こりません。2021年2月現在 Maintenance LTS の v10.x, v12.x で発生します。CodeSandbox は現在 v10.23.x を使用しているため再現できます。

Warning の再現

まずは CodeSandbox の Next.js 環境で再現するコードを用意しました。

Chrome の Console に表示された Warning のスクリーンショット
Warning: Text content did not match. Server: "2/11/2021" Client: "2021/2/11"
    in div (at pages/index.js:5)
    in IndexPage (created by App)
    in App
    in ErrorBoundary (created by ReactDevOverlay)
    in ReactDevOverlay (created by Container)
    in Container (created by AppContainer)
    in AppContainer
    in Root

原因箇所

前述の Warning では今日の日付を表示している部分がサーバーでは 2/11/2021 となっていてブラウザーでは 2021/2/11 となっているのがわかります。
該当コードは下記のように "ja-JP" と日本にあわせた locale を指定しています。

{new Date().toLocaleDateString("ja-JP")}

ブラウザー側では日本で一般的な YYYY/MM/DD 表記になっており意図通りですが、サーバー側では MM/DD/YYYYとアメリカで一般的な表記になっているのが Warning の理由です。

Node.js の ICU

Node.js は ICU(International Components for Unicode)という Intl オブジェクト(国際化 API)などが使用する Locale に関わるデータを指定してビルドされます。

v12.x までは small-icu という限られた言語(と地域)に絞った Locale 情報をもつビルド設定がデフォルトになっていました。
small-icu には日本と日本語に関わる Locale 情報がふくまれないためサーバーサイドでの日付出力が MM/DD/YYYY となります。

この問題を解決するには Node.js に small-icu ではなく、すべての言語情報をふくむ full-icu 相当の Locale データを与える必要があります。

解決策1 npm full-icu の導入

npm の full-icu を利用すれば small-icu を指定してビルドした Node.js でも full-icu が利用可能になります。 NODE_ICU_DATA 環境変数に full-icu のパスを指定して実行します。

$ yarn add full-icu
$ NODE_ICU_DATA=node_modules/full-icu yarn dev

ただし、チームメンバーの各開発環境で毎回環境変数を付与したり shell のドットファイルに export を記述するのは手間になりますし、CI, CD などの対応も考慮する必要があります。
そのため package.json の scripts で指定しましょう。

  "scripts": {
    "dev": "NODE_ICU_DATA=node_modules/full-icu next dev",
    "build": "NODE_ICU_DATA=node_modules/full-icu next build",
    "start": "NODE_ICU_DATA=node_modules/full-icu next start",
  },

この方法は Next.js の開発元である Vercel も自社サービスのドキュメントに記載しています。

解決策2 Node.js をビルドしなおす

もうひとつの方法は full-icu サポートの Node.js を自前でビルドして対応する方法です。
ビルド方法など詳しくは解説しませんが、下記ドキュメントに従って --with-intl=full-icu を指定して目的のバージョンの Node.js をビルドすることで解決します。

ただ、開発者が1〜2名でインフラも自前のメンテナンスが行き届いていれば可能かもしれませんが、チームメンバーが増えてきたり CI などを頻繁にまわすようになってくると現実的ではなくなってくる可能性があります。

またインフラ構成まで変更可能なのであれば、デフォルトで full-icu サポートとなった 14.x 系へのアップデートの検討をおすすめします。

別のアプローチ

若干記事の趣旨から脱線しますが、 Node.js やブラウザの国際化対応に頼らず、自前で locale データを持っている date-fns や Day.js などのライブラリを利用するアプローチもあります。

本来ブラウザ側では気にしなくて良い課題を解決するために locale 情報を含んだ js を配信することになるのでコードベースは膨れてしまいますが、複雑な日付操作などがあるのであればこれらのライブラリを使うのが一般的かと思います。
単純な日付の処理についてもすべてライブラリを利用することで SSR とずれる問題を解消できます。

Node.js の full-icu サポート

2018年に「Building with full-icu by default」という GitHub Issue で full-icu をデフォルトにしないかという議論が始まっていたのは見ていたのですが、 v13, v14 でデフォルトが変更されていたことに気づいていませんでした。

一時期よくこの問題に遭遇していたのですが、久しぶりに遭遇したので記事にしようと思い調べたところ v14 では問題ないことを知りました。若干賞味期限切れの話題ですが、 small-icu がデフォルトになっている Maintenance LTS の v10, v12 はまだサポートが続いているので、次にハマったときのために記事として残しておきます。

ReleaseInitial ReleaseEnd-of-life
v102018-04-242021-04-30
v122019-04-232022-04-30
v142020-04-212023-04-30
※ Node.js のリリースプランから抜粋

弊社ではJamstackの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Harada Naotaka

受託と事業会社の両方を経験し、沢山の事業を見てみたい気持ちで Gaji-Labo を共同創業。普段は雑用やったりプロジェクトマネジメントやったり、たまにフロントエンドのコードを書いたり。直近は Gaji-Labo をデザイン会社に転換していく課題に挑戦中。期待値コントロールにステ全振り。