👷

[NextJS] Minified React Error(#418, #423, #425)にPlaywrightで立ち向かう

2024/04/19に公開

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に組み込むことです。

  1. Github Pull Requestへの変更で以下をトリガー
  2. Vercel deploy で テスト環境に配布
  3. 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!

FunnySideUp

Discussion