🦍

dynamic exportを使いClient sideだけでrenderingを実現する

2024/05/15に公開

はじめに

Next.jsでのRenderingにおいて、App RouterかPagesかに関わらず、SSRがデフォルトの挙動となっている為、App Routerで 'use client' pragmaを指定しても SSR されてしまうことがあります。
ですが往々にして特定のコンポーネントをクライアントサイドでのみレンダリングしたい場合があると思います。

この記事では、Next.jsのdynamic関数を使用して、クライアントサイドでのみコンポーネントをレンダリングする方法について備忘録も兼ねて説明します。

ちらつき(Flickering)の問題

実装をおこなっていると時折この問題にぶつかることがあると思いますが、SSR(サーバーサイドレンダリング)では、サーバーがHTMLを生成した後クライアントに送信するフローの為、このプロセス中に適用される前の一瞬の間に初期値でのComponentが表示されることがあり、ユーザーにとってちらつきが発生することがあります。
自身も最近ではnext-themesのようなテーマ管理ライブラリを使用した際にこの問題に遭遇しました。

dynamic関数を使用すると

useEffectとuseStateを使い、mountedを検知することでちらつき問題を解消させることもできますが
Next.jsのdynamic関数を使用すると、特定のコンポーネントをクライアントサイドでのみレンダリングするように設定できます。以下、この例を示し内容を説明します。

サンプルコード全体

以下、今回のコードの全体像になります。

"use client";

import dynamic from "next/dynamic";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "./ui/button";

const ToggleMode = () => {
  const { theme, setTheme } = useTheme();
  const dark = theme === "dark";

  return (
    <Button
      variant="outline"
      size="icon"
      onClick={() => setTheme(dark ? "light" : "dark")}
    >
      {dark ? (
        <Sun className="hover:cursor-pointer hover:text-primary" />
      ) : (
        <Moon className="hover:cursor-pointer hover:text-primary" />
      )}
    </Button>
  );
};

export default dynamic(() => Promise.resolve(ToggleMode), { ssr: false });

1. 必要なモジュールのインポート

"use client";

import dynamic from "next/dynamic";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "./ui/button";
  • 'use client' pragmaを指定
  • dynamicをimport
    ※あとは適宜、自身のケースにあったコンポーネントのimportに読み替えてください

2. ToggleModeコンポーネントの定義

ここでは例としてToggleModeコンポーネントを定義し、現在のテーマに基づいてアイコンを表示しクリックするとテーマを切り替えられるようにします。

const ToggleMode = () => {
  const { theme, setTheme } = useTheme();
  const isDark = theme === "dark";

  return (
    <Button
      variant="outline"
      size="icon"
      onClick={() => setTheme(isDark ? "light" : "dark")}
    >
      {isDark ? (
        <Sun className="hover:cursor-pointer hover:text-primary" />
      ) : (
        <Moon className="hover:cursor-pointer hover:text-primary" />
      )}
    </Button>
  );
};

この状態の実装でちらつき(Flickering)が起きる主な原因は、next-themesライブラリがクライアントサイドでテーマを切り替えるまでの間に、サーバーサイドでレンダリングされた初期状態が一瞬表示されてしまうことによるものです。この問題は、以下のような要因によって発生します:

サーバーサイドレンダリング(SSR)

冒頭でも述べたようにNext.jsはデフォルトでサーバーサイドレンダリングを使用します。そのため、初期レンダリング時にはサーバーが生成したHTMLがクライアントに送られます。この時点では、テーマの設定がクライアントサイドのJavaScriptで適用されていないため、デフォルトのテーマが表示されます。

クライアントサイドのテーマ適用の遅延

クライアントサイドでuseThemeフックが初期化され、テーマが適用されるまでの間にわずかな遅延が発生します。この遅延の間にデフォルトのライトテーマやサーバー側のテーマが表示され、その後クライアントサイドのJavaScriptによってテーマが切り替えられます。この切り替えが視覚的に認識されることでちらつきが発生します。

3. dynamic関数を使用してエクスポート

最後に、dynamic関数を使用してコンポーネントを非同期にエクスポートします。
ssr: falseオプションを設定することで、サーバーサイドレンダリングを無効にし、クライアントサイドでのみレンダリングされるようにします。

export default dynamic(() => Promise.resolve(ToggleMode), { ssr: false });

余談

読み込む側のcomponentでdynamic importを行うこともできますが何度も読み込み側でdynamic importを書くのは面倒なのでexportを使いました。

また、Loading時のcomponentを用意すればimportのときと同じく引数に渡せるのでこうしておくと各componentを使い回す際にdynamicでの対応が必要かを読み込み側のcomponentで意識せず使うことができます。

export default dynamic(() => Promise.resolve(ToggleMode), { ssr: false, loading: () => <Loading />, });

まとめ

以上によりToggleModeコンポーネントはクライアントサイドでのみレンダリングされるようになり、サーバーサイドレンダリングによるちらつきを防ぐことができるようになります。
dynamic関数を使用することはNext.jsの柔軟なレンダリングオプションを活用し、ユーザー体験を向上させることができるので今後も使っていきたい手法です。

参考

Lazy Loading

Discussion