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 環境で再現するコードを用意しました。
- CodeSandbox:
https://codesandbox.io/s/hungry-napier-tvtkz?file=/pages/index.js - Demo:
https://tvtkz.sse.codesandbox.io/
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 に関わるデータを指定してビルドされます。
- Intl
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl - International Components for Unicode
http://site.icu-project.org/ - Node.js – Internationalization support
https://nodejs.org/api/intl.html#intl_internationalization_support
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 はまだサポートが続いているので、次にハマったときのために記事として残しておきます。
Release | Initial Release | End-of-life |
---|---|---|
v10 | 2018-04-24 | 2021-04-30 |
v12 | 2019-04-23 | 2022-04-30 |
v14 | 2020-04-21 | 2023-04-30 |
弊社ではJamstackの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!
求人応募してみる!