[NextJS] Minified React Error(#418, #423, #425)にPlaywrightで立ち向かう
NextJSで地味によくはまったポイントのノウハウシェアです。
Minified React Error自体は多様なケースがあるようで、この記事はその中の一部(#418, #423, #425)を対象にしています。
発生原因
NextJSでサーバーサイドレンダリング結果が、クライアント側の初期描画内容と一致しない場合に発生。
発生例
1. ブラウザサイズに依存した表示分け
{isMd ? (
mdサイズ以上でのコンポーネント
) : (
それ未満でのコンポーネント
)}
2. (レアケース)next-intlの翻訳テキスト内の連続改行 "\n\n"
こんな具合で改行コードを立て続けに入れただけでMinified Error。
もしか、next-intlのバージョンなどにもよるかもしれない。
- NG:
"keyForMessage": "Welcome to ~~~!\n\nHere you can ~"
- OK:
"keyForMessage": "Welcome to ~~~!\nHere you can ~"
放置すると何が良くないか?
NextJSでサーバーサイドレンダリングをしている箇所において Minified React Errorを放置すると、
せっかくサーバー側で生成済みのコンテンツがあるのに、クライアントで作り直しが発生してしまう。
結果、初期表示のパフォーマンスが劣化する。
苦しいポイント
- とにかく、どこで発生しているかヒントがなくて、デバッグしにくすぎる。エンジニア工数が飛んでいく。
- SPAの場合に、ページ間遷移だと発生しないが、該当ページのURLを開いて直接ランディングする場合に生じたりする。(サーバーサイドのレンダリングとの差分をもとにエラーを吐くので当然ではあるが)
調査方法
1.レンダリング結果の比較
以下のAとBを比較する。(もしくは、紐づく PullRequestがあればコード差分を確認する)
- A: サーバーサイドのレンダリング結果(=該当ページのinitialレスポンスのdocumentのbodyを参照)
- B: クライアントのレンダリング結果(=Chrome devtoolでdocument bodyを参照)
2.判定
- 差分があるわかりやすい問題であればラッキー。
- 差分が一見無いことや、cssなどの許せる差分が肥大すぎて調査しにくい場合もある。
3.深堀り
一見して原因がわからない場合、ページ内のコンポーネントや表示セクションを小分けして非表示化できるようにServerSidePropsなどで出しわけ処理を行って、しらみ潰しに原因箇所を特定する。コピペして少しずつ内容を取捨したページを並べてどのケースでエラーが出なくなるかを試しても良い。
解消方法
1.ブラウザサイズによるコンポーネント出し分けをやめる
言い換えると、CSS制御に寄せてレンダリング内容は統一する。
具体的には、例えばtailwindcssの md:w-12
など、breakpointの指定を活用。
2.dynamic importの活用
サーバーサイドレンダリングをあえてしない要素を囲い出す。
よくある手口が、<NoSSR>などとdynamic importを支援するコンポーネントを切って、それで覆うことでサーバーサイドレンダリングの範疇から外す。
// trick found this article:
// https://plainenglish.io/blog/how-to-solve-hydration-error-in-next-js
import React, { type PropsWithChildren } from "react";
import dynamic from "next/dynamic";
const MockChildren = (props: PropsWithChildren) => (
<React.Fragment>{props.children}</React.Fragment>
);
export const NoSSR = dynamic(() => Promise.resolve(MockChildren), {
ssr: false,
});
<NoSSR>
サーバーサイドでレンダリングさせたくない内容(= Error原因)
</NoSSR>
3.(レアケース)i18nの言語ファイル上の多重改行("\n\n")を諦める
などなど...
[本題]予防法
色々書きましたが、そもそも機能開発や改善をする中ですぐに気付けたら、
コード差分内に問題があることが自明なので、簡単に問題を解消できる!
つまりは、CIでカバーした方が良い。
そこで弊社サービス Arounds で導入している方法は、
Playwrightのコンソールログの監視をCIに組み込むことです。
- Github Pull Requestへの変更で以下をトリガー
- Vercel deploy で テスト環境に配布
- Checkly run で テスト環境を対象にCI実行
4. → この中でPlaywrightを用いたコンソール監視を実装。
export const checkConsoleError = async (page: Page, targetPath: string) => {
const client = await page.context().newCDPSession(page);
await client.send("Runtime.enable");
const messages = [] as string[];
client.on("Runtime.exceptionThrown", (payload) => {
messages.push(
payload.exceptionDetails.exception?.description || "no description",
);
});
const messageLength = messages.length;
const pageUrl =
targetPath.length > 0 ? `${TargetUrl}/${targetPath}` : TargetUrl;
await page.goto(pageUrl, { waitUntil: "load" });
await page.waitForTimeout(2000);
const hasReactMinifiedError = messages.some(
(message) =>
message.includes("Minified React error") ||
message.includes("#425") ||
message.includes("#423") ||
message.includes("#418"),
);
expect.soft(hasReactMinifiedError).toBeFalsy();
expect.soft(messages.length).toEqual(messageLength);
await page.close();
};
Minified React Errorの有無と、コンソールログに初期ロード時点で意図しないログが出ていないかを確認しています。
あとはこのコードをspecファイルから呼び出して、任意のページパスを検証していくだけです。
この記事で助かったぜ!という方は、
趣味や愛用品があなたを物語ってくれる Aroundsにぜひ遊びに来てください♪
(私含め)口下手も多かろうエンジニアチームでのコミュニケーション促進も果たせると思うので
好きな漫画や信仰している技術書、デスク周りのガジェット、思い出の旅行先など集めて教えてください^^
Happy hacking!
Discussion