kk-web

Next.js で window にアクセスする方法

2020-08-04

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 はサーバ側で実行されないためです。

そのため、

  1. サーバー側で JavaScript が実行される(pathname は空文字)
  2. サーバー側からクライアント側に文字列が渡され、描画が行われる(pathname は空文字)
  3. クライアント側で JavaScript が実行され、描画が更新される(実際には差分が存在しないため、画面上に変化は起きない)(pathname は空文字)
  4. クライアント側で useEffect が実行される(pathname は "/")
  5. クライアント側で描画が更新される(pathname は "/")

といったフローが取られます。

で、1 ~ 3 までのフロー内で返される文字列に差分が発生してはいけないため、必然的に useEffect で window オブジェクトにアクセスすることになる、というわけです。


今回は window オブジェクトに着目しましたが、storage や cookie なども同様の問題が発生します。

ここらへんの理解ってむつかしーですよね…自分も未だについていけていない部分も多いです。

Redux とかが絡んでくるとけーっこううんざりしてくるんですが、これって自分だけなんでしょうか。

© 2018 kk-web