概要
すでにリリースしているRemix ✕ CloudflareのWebアプリケーションの本番環境の全ページでコンソールにいくつかのエラーが出ていることをエンジニアの先輩が見つけてくださった。先輩方のお知恵をいただきながらエラーを消すために頑張ったので簡単にまとめておく。
コンソールに出たエラー内容
コンソールに出ていたエラー内容を紹介する。
Uncaught Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
本番環境は本番環境用のエラー出力になっているため詳細な情報は記載されていないらしい。
とりあえずエラー内容に記載されているリンク先を見てみる。
エラーの詳細の和訳を下記に記載する。
サーバーがレンダリングした HTML がクライアントと一致しなかったため、ハイドレイティングに失敗しました。 その結果、このツリーはクライアント上で再生成されます。 これはSSR化されたクライアントコンポーネントが次のような場合に 起こりえます:
- サーバ/クライアントのブランチ `if (typeof window !== 'undefined')`.
- 呼び出されるたびに変化する `Date.now()` や `Math.random()` のような変数入力
- ユーザのロケールでの日付書式がサーバと一致しない。
- HTMLと一緒にスナップショットを送信せずに、外部でデータを変更する。
- 無効なHTMLタグの入れ子。
クライアントがブラウザ拡張機能をインストールしており、Reactが読み込まれる前にHTMLをいじってしまう場合にも発生する。https://react.dev/link/hydration-mismatch[引数の欠落]
自身の理解だと、「サーバーサイドでレンダリングされた画面とクライアント側の画面に差分があってSSRの意味ないよ」的なことだと理解した。
問題箇所の特定
すでに先輩が「十中八九Googleタグマネージャーのタグを追加している部分が怪しいと思う〜!」と教えてくださった。念の為別角度からも原因がGoogleタグマネージャーのタグであるかをチェックしてみる。
全画面で同様のエラーが出ることからおそらくroot.tsxに追加した内容に問題があると推測した。
これは別の先輩から教わった方法だが、Chromeの拡張機能を一時的に全てOFFにしたあとに、とにかく問題になってそうな(今回ならSSRでレンダリングしたJSX)部分のコードを消してみて徐々に問題箇所を特定してみた。
やはり下記のコードを削除したらエラーは出なかった。
// isProductionがtrueじゃないと本番で出たエラーと近いエラーがローカル開発環境で出ないためコメントアウト
//{isProduction && (
<>
{/* Google Tag Manager */}
<script dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GoogleタグマネージャーのID');
`
}} />
{/* End Google Tag Manager */}
</>
//)}
もしかすると↑の関数がクライアントに返ったあとに実行されてscript要素が埋め込まれているのかもしれない。
解決に向けたトライ ①(解消せず)
下記の情報を参考に解消を試みたが、自身の環境ではエラーを解消することはできなかった。他の要因の可能性もあるが、new Date()
の部分を消すとエラーが発生しないように思えた。
解決に向けたトライ ②(解決)
下記の情報を参考に解消を試みたところ、エラーは出ずそれっぽいscript要素が追加されるようになった。
一部if分岐などがハードコーディングでbool値を評価に用いておりお見苦しいコードではあるがこんな感じだ。
export function Layout({ children }: { children: React.ReactNode }) {
const GTM_ID = 'GoogleタグマネージャーのID';
useEffect(() => {
if (/* isProduction */ false) {
addGtmScript(GTM_ID);
}
}, [GTM_ID, /* isProduction */]);
return (
// ベースとなるテンプレートのJSXを返す
// Googleタグマネージャーの情報はJSがOFFの場合のnoscriptの要素のみ記載
);
}
let gtmScriptAdded = false;
declare global {
interface Window {
[key: string]: object[];
}
}
function addGtmScript(GTM_ID: string) {
if (!GTM_ID || gtmScriptAdded) {
return;
}
// Code copied from GTM console + added type annotations.
(function (w: Window, d: Document, s: "script", l: string, i: string) {
w[l] = w[l] || [];
w[l].push({
"gtm.start": new Date().getTime(),
event: "gtm.js",
});
const f = d.getElementsByTagName(s)[0];
const j = d.createElement<"script">(s);
const dl = l != "dataLayer" ? "&l=" + l : "";
j.async = true;
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
f.parentNode?.insertBefore(j, f);
})(window, document, "script", "dataLayer", GTM_ID);
gtmScriptAdded = true;
}
上記の様にしてuseEffect()の引数のコールバック関数内部のif文の評価対象をtrueにしたらhead要素内部に下記の様な内容が追加された。
<script async srv="https://www.googletagmanager.com/gtm.js?id="GoogleタグマネージャーのID">
このscript要素が入っていてほしいだけなのでこれで一旦問題なさそう。
Googleタグマネージャー側の仕様変更に強い方法を先輩に手引してもらいながら考える
まずは先にGoogleタグマネージャーでタグを発行するとどのような指示が与えられるか見てみる。
タグを作ると下記のように「表示されている内容をHTMLの画面のコードの何処かに入れてほしい」というお達しが来る。
ちなみに、headとbodyに追記するコードがGoogleタグマネージャーから提供されるが、head側が通常用、body側がブラウザの設定でjavascriptが無効化されているとき用らしい。
今回自分が実施した内容はheadに追記するコードがクライアント側で動作した結果追加されるscript要素を先に用意した。
このscript要素が入っていてほしいだけなのでこれで一旦問題なさそう。
最終的にGoogleタグマネージャーがページをトラッキングするためのscript要素が入っていればいいということである。
せっかくGoogleタグマネージャーがコードを提供してくれているのだからなるべくコピペだけで済ませたい。なので下記のように記載してみた。
export function Layout({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (/* isProduction */ true) {
eval(`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GoogleタグマネージャーのID');`)
}
}, [/* isProduction */]);
これならGoogleタグマネージャーから提供されたコードのhead側のscript要素の中の記載をevalの引数のバッククォートの中に貼り付けるだけで良い。