Next.js で window にアクセスする方法
SSR 環境下で最初につまずくことと言えば、やはり window オブジェクトへのアクセスなのかなーと思います。
Create React App と同じようなノリでコーディングをしていたらエラーを吐いた!みたいなことはザラにありそうです。
今回は、Next.js 環境で window オブジェクトへアクセスしてみようと思います。
正しいアプローチ
まず正しいアプローチは以下のような形となります。
const Pages: NextPage = () => {
const [currentPathname, setCurrentPathname] = useState("");
useEffect(() => {
const {
location: { pathname },
} = window;
setCurrentPathname(pathname);
}, [setCurrentPathname]);
return <div>{currentPathname}</div>;
};
ただ window オブジェクトにアクセスするだけなのに、 useEffect を噛ませる回りくどい方法を取らなければいけないのか。
最初から掘り下げてみようと思います。
まず、一番シンプルに書こうと思ったら、こんな感じですかね?
const Pages: NextPage = () => {
const {
location: { pathname },
} = window;
return <div>{pathname}</div>;
};
SSR を噛ませていなければ、これで動きます。
一方 SSR の場合、つまりファーストビューの描画前はサーバ側に window オブジェクトが存在しないため、サーバー側のターミナルに以下のエラーが出力されます。
ReferenceError: window is not defined
ちなみに、あくまでファーストビューの描画前に引っかかるため、開発時にホットリロードが回っているとこのエラーが出力されないこともしばしば…十分気をつけましょう。
なので、以下のコードも同様の理由で動きません。
const Pages: NextPage = () => {
const pathname = useMemo(() => {
const {
location: { pathname },
} = window;
return pathname;
}, []);
return <div>{pathname}</div>;
};
useMemo 関数はサーバ側でも実行されるので、こちらもファーストビューの描画前にサーバー側のターミナルにエラーが吐かれます。
では、以下のように書いたらどうでしょうか。
const Pages: NextPage = () => {
const pathname = useMemo(() => {
if (typeof window !== "object") {
return "/";
}
const {
location: { pathname },
} = window;
return pathname;
}, []);
return <div>{pathname}</div>;
};
このコードはたまに見かけますし、トップページに限り、このコードはエラーを吐かずに動きます。
ちなみに、以下のように書くとクライアント側のコンソールに warning が出力されます。
const Pages: NextPage = () => {
const pathname = useMemo(() => {
if (typeof window !== "object") {
return "";
}
const {
location: { pathname },
} = window;
return pathname;
}, []);
return <div>{pathname}</div>;
};
useMemo 内の戻り値を変えただけです。
この場合、動作はするけれど、SSR と CSR の仕様には合っていない状態になります。
warning の内容は以下のとおりです。
Warning: Text content did not match. Server: "" Client: "/"
サーバー側が返すコードとクライアント側で描画したコードで差分が発生しているよ、ということですね。
なので、"/" を返してやると、トップページでは同じ pathname
が取得されるため、warning が吐かれなくなるわけですね。
とはいえ、これはたまたま偶然同じ値が取得されたに過ぎないため、もちろん良いロジックではありません。
ではなぜ最初に書いたアプローチでは正常に処理が行われているかというと、useEffect はサーバ側で実行されないためです。
そのため、
- サーバー側で JavaScript が実行される(pathname は空文字)
- サーバー側からクライアント側に文字列が渡され、描画が行われる(pathname は空文字)
- クライアント側で JavaScript が実行され、描画が更新される(実際には差分が存在しないため、画面上に変化は起きない)(pathname は空文字)
- クライアント側で useEffect が実行される(pathname は "/")
- クライアント側で描画が更新される(pathname は "/")
といったフローが取られます。
で、1 ~ 3 までのフロー内で返される文字列に差分が発生してはいけないため、必然的に useEffect で window オブジェクトにアクセスすることになる、というわけです。
今回は window オブジェクトに着目しましたが、storage や cookie なども同様の問題が発生します。
ここらへんの理解ってむつかしーですよね…自分も未だについていけていない部分も多いです。
Redux とかが絡んでくるとけーっこううんざりしてくるんですが、これって自分だけなんでしょうか。