例えば Next.js でこんなコンポーネントを表示しようとしても、

const Component = () => {
  return <p>This is {window.location.href}</p>;
};

ReferenceError: window is not defined になります。

これを回避するには、useEffect の中で window オブジェクトにアクセスする必要があります。回避と書きましたがこれが正しい対応方法です。

でも、そもそもこの部分は CSR 前提で作っているから SSR は使わないのよ、と言う場合は SSR を無効化することもできます。

next/dynamic の Dynamic Import を使いましょう

こんな感じでコンポーネントが export されているとします。

components/index.tsx

const Component = () => {
  return <p>This is {window.location.href}</p>;
};

export { Component };

変更前

それを index ページが import して表示しているとします。このままだと先程のエラーが出てしまいます。

pages/index.tsx

import { Component } from 'src/components';

export default Component;

変更後

では index ページを SSR しないように設定します。それが以下です。

pages/index.tsx

import dynamic from 'next/dynamic';

export default dynamic(
  async () => {
    const module = await import('src/components');
    return module.Component;
  },
  { ssr: false }
);

こうすると dynamic import されたコンポーネントは必ず CSR でのみ実行されます。

useEffect を挟まずとも window オブジェクトにアクセス可能です。他には Next.js の router.query が初回に undefined を返すこともなくなります。一発でクエリストリングが取得できます。

dynamic import はコンポーネント単位で行えるため、ページの一部分のみを dynamic import することでその部分だけを SSR 無効化させることも可能です。

アプリ全体を CSR 化してしまうなら

アプリ全体を一括で CRS 化するのであれば、CSR 用の高階コンポーネントを作成して _app.tsx で他のコンポーネントを囲ってしまうのが楽です。

components/CSRInner.tsx

const CSRInner = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
  return <>{children}</>;
};

export default CSRInner;

components/CSR.tsx

import dynamic from 'next/dynamic';

const CSR = dynamic(() => import('./CSRInner'), { ssr: false });

export default CSR;

pages/_app.tsx

import CSR from 'src/components/CSR';
import type { AppProps } from 'next/app';

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <CSR>
      <Component {...pageProps} />
    </CSR>
  );
};

export default App;

リファレンス

公式のわかりやすいドキュメントがありますので、後の詳細はそちらをぜひご参照ください。

Advanced Features: Dynamic Import | Next.js