0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ほぼ備忘録】next-themes でSSR時にもテーマがわかるようなコンポーネントを作ってみた

0
Last updated at Posted at 2026-01-27

方針

Cookie を使えばリクエストのときに送信されるため SSR ができる

コード

layout.tsx
 export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  // サーバーで Cookie から取得
  const theme = await getThemeFromCookie();
  return (
    <html lang="ja" suppressHydrationWarning>
      <head />
      <body className={`antialiased`}>
        <ThemeProvider
          attribute="class"
          defaultTheme={theme}
          enableSystem
          disableTransitionOnChange
        >
          {children}
          {/* Cookie が消されたとき用のコンポーネント */}
          <ColorThemeConflictFix cookieTheme={theme} />
        </ThemeProvider>
      </body>
    </html>
  );
}

Cookie がユーザーの操作で消されることもあるのでそれを強制的に直します

lib/color-mode/color-theme-conflict-fix.tsx
"use client";

import { useTheme } from "next-themes";
import { useLayoutEffect, useRef } from "react";
import { Theme } from "./common";

export default function ColorThemeConflictFix({
  cookieTheme,
}: {
  cookieTheme: Theme;
}) {
  const firstrendering = useRef(true);
  const { theme, setTheme } = useTheme();
  useLayoutEffect(() => {
    if (firstrendering.current) {
      if (theme != cookieTheme) {
        setTheme(cookieTheme);
      }
      firstrendering.current = false;
    }
  }, [cookieTheme, theme, setTheme]);
  return null;
}
lib/color-mode/common.ts
export const COLOR_THEME_COOKIE_KEY = "color-theme";

export type Theme = "light" | "dark" | "system";
export const THEMES = ["system", "light", "dark"] satisfies Theme[];
lib/color-mode/cookie-server
import "server-only";

import { cookies } from "next/headers";
import { COLOR_THEME_COOKIE_KEY, Theme, THEMES } from "./common";

export async function getThemeFromCookie(): Promise<Theme> {
  const cookieStore = await cookies();
  const savedCookie = cookieStore.get(COLOR_THEME_COOKIE_KEY)?.value;
  if (!savedCookie) {
    return "system";
  }
  if ((THEMES as string[]).includes(savedCookie)) {
    return savedCookie as Theme;
  } else {
    return "system";
  }
}

/lib/color-mode/cookie-client.ts
import { COLOR_THEME_COOKIE_KEY, Theme } from "./common";

export function setThemeCookie(theme: Theme) {
  const maxAge = 60 * 60 * 24 * 365;
  document.cookie = `${COLOR_THEME_COOKIE_KEY}=${encodeURIComponent(theme)}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
}

そしたら切り替え用のコンポーネントを作ります

/lib/color-mode/toggle-color-mode.tsx
export default async function ColorModeToggle() {
  const theme = await getThemeFromCookie();
  return <ColorModeToggleClient initMode={theme} />;
}
/lib/color-mode/toggle-color-mode-client.tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { LaptopMinimal, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { THEMES, type Theme } from "./common";
import { setThemeCookie } from "./cookie-client";

function getNextTheme(currentTheme: Theme): Theme | null {
  const currentIndex = THEMES.indexOf(currentTheme);
  if (currentIndex < 0) {
    return null;
  }
  let newIndex = currentIndex + 1;
  if (newIndex >= THEMES.length) {
    newIndex = 0;
  }
  return THEMES[newIndex];
}

export default function ColorModeToggleClient({
  initMode,
}: {
  initMode: Theme;
}) {
  const { theme, setTheme } = useTheme();
  const computedTheme = (theme ?? initMode) as Theme;

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          onClick={() => {
            const nextTheme = getNextTheme(computedTheme);
            if (!nextTheme) return;
            setThemeCookie(nextTheme);
            setTheme(nextTheme);
          }}
        >
          {computedTheme === "light" && (
            <Sun className="h-[1.2rem] w-[1.2rem]" />
          )}
          {computedTheme === "dark" && (
            <Moon className="h-[1.2rem] w-[1.2rem]" />
          )}
          {computedTheme === "system" && (
            <LaptopMinimal className="h-[1.2rem] w-[1.2rem]" />
          )}
        </Button>
      </TooltipTrigger>
      <TooltipContent>
        <p>Toggle Mode</p>
      </TooltipContent>
    </Tooltip>
  );
}

これでハイドレーションエラーはおきません (あとから Cookie 削除されない限りは)

あとがき

適当に作ったのでもし使う場合にはお好みで整えてください
あとページまるごとのキャッシュは基本無理になるので気をつけてください

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?