先週のブログに書いたようなコードを仕事で書いていて、不思議なエラーに出会ってしまいました。
環境は以下のとおりです、
- Next.js v14.1.0
- Pages Routerモード
出会ったエラー
以下のReactのプログラムはViteで使った環境では動作し、ブラウザー画面の幅が表示されます。
import { useMemo } from "react";
export default function Home() {
const width = useMemo(() => {
return window.innerWidth;
}, []);
return (
<>
<p>Width: {width}</p>
</>
);
}
しかし、Next.jsで作った環境では以下のようなエラーが発生してしまいます。
windowオブジェクトはブラウザーが標準でもっていいるオブジェクトです。なぜそれが無いの?
コードを以下のように修正すると動作します。なぜ?
import { useState, useEffect } from "react";
export default function Home() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return (
<>
<p>Width: {width}</p>
</>
);
}
いろいろと調べて、やっと判りました。Next.jsはデフォルトで、Server Side Rendering(SSR)で動作しています。
- サーバー側でReactコードを実行し、HTMLに変換できる部分はサーバー側で作成
- 開発モードではローカル開発サーバーで実行
- それをブラウザーにHTMLを送り表示
- その後、ReactコードをコンパイルしたJavaScriptをブラウザーに送る
- JavaScriptを実行し必要な部分のみを更新
- この処理はハイドレーション(Hydration)と呼ばれています
という動作をしています。エラーになったコードではuseMemo()
がサーバー側(開発サーバー)のNode.jsで実行するので、windowオブジェクトは無くエラーになったわけです。
useEffect()
を使ったコードが正しく動くのは、useEffect()
は画面表示後に実行されるのでブラウザー(ハイドレーション)での実行が保証されるわけです。
おまけ
次のコードをNext.jsで実行すると、
import { useMemo } from "react";
export default function Home() {
const width = useMemo(() => {
return Math.random();
}, []);
return (
<>
<p>Width: {width}</p>
</>
);
}
サーバーで実行した時とブラウザーで実行した時で乱数の値が違うので、このような不思議なエラーになるのです。😅
SSRを止めるには
Next.jsのドキュメントにはWith no SSRという項目があり、SSRを実行しないようにも設定できます。
今回のコード等で試すには、共通ページpages/_app.tsx
を以下のようにするとSSRが止まり、Vite等で作ったアプリと同じように動作します。
import type { AppProps } from "next/app";
import dynamic from "next/dynamic";
import React from "react";
const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
export default dynamic(() => Promise.resolve(App), {
ssr: false,
});
まとめ
ReactオフィシャルドキュメントのReact プロジェクトを始めるではReactプロジェクト作成にはNext.js等のフレームワークを薦めていますが、今回のエラーのようにフレームワークの特性から起きるエラーに出会う可能性もあります。
モヤモヤ🤔