🌗

next-themesについてのメモ(`theme`と`resolvedTheme`の使い分け/Hydration Error対策)

に公開

ドキュメント読んで微妙にわからなかった部分の備忘録。

TL;DR

  • themeは「何のテーマを選んでいるか」を返す(light or dark or system)
    systemに明示的に切り替えたい場合などにはこっち

  • resolveThemeは「実際に何のテーマが使われているか」を返す( light or dark)
    light/darkを切り替えたいだけならこっち

  • theme,resolveThemeを使用する処理はマウントされてからじゃないとダメ

next-themes基礎知識

Next.js用のテーマ管理ライブラリです。App Routerならlayout{children}<ThemeProvider>でラップするだけで簡単にダークモードを実装できます。

// app/layout.jsx
import { ThemeProvider } from 'next-themes'

export default function Layout({ children }) {
  return (
    <html suppressHydrationWarning>
      <head />
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

tailwindを使う場合は以下の設定を追加してあげて、

ts.config.json
const config: Config = {
  // ...
  darkMode: ["selector", ".dark"],

classNameに追加してあげるだけでOKです。簡単!

<p className="text-black dark:text-white">text</p>

ドキュメントはこちら
https://github.com/pacocoursey/next-themes

テーマの切り替え方法

テーマの切り替えにはuseThemesetThemeを使います。
これに渡したテーマがアプリ全体で共有されることになります。

import { useTheme } from 'next-themes'

const ThemeChanger = () => {
  const { setTheme } = useTheme()
  // Hydration Error対策(後述)
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <div>
      <button onClick={() => setTheme('light')}>Light Mode</button>
      <button onClick={() => setTheme('dark')}>Dark Mode</button>
      <button onClick={() => setTheme('system')}>Dark Mode</button>
    </div>
  )
}

基本はlight,dark,systemの3種類のどれかを使うと思いますが、自作のテーマを指定することもできます。systemはOSで指定されているやつですね。Chrome Dev ToolsのEmulate CSS media feature prefers-color-schemeで指定できるあれです。

https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-color-scheme

https://developer.chrome.com/docs/devtools/rendering/emulate-css?hl=ja

テーマの使用

現在のテーマはthemeresolveThemeを用いて受け取ることができます。
が、この二つはちょっとした違いがあるので気を付ける必要があります。
まずはドキュメントを見てみましょう。

theme: Active theme name

resolvedTheme: If enableSystem is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to theme

ちょっとわかりづらいですが、要はsystemが指定された時の振る舞いが違うだけです。

  setTheme("system"); // themeにsystemを指定.OSはダークモード設定
  console.log("theme is", theme); // theme is system
  console.log("resolvedTheme is", resolvedTheme); // resolvedTheme is dark

themeはそのままsystemとなりますが、resolvedSystemならsystemが結果的に何になってるのかまで解決してくれます。

themeresolvedThemeの使い分け

resolveThemeの使用例

ただテーマを使用する+切り替えるだけならresolvedThemeで十分です。

const DarkModeToggle = () => {
  const { resolvedTheme, setTheme } = useTheme();

  const handleToggle = () => {
    setTheme(resolvedTheme === "light" ? "dark" : "light");
  };

  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <button onClick={handleToggle} >
      {resolvedTheme === "light" ? (
        <MoonIcon />
      ) : (
        <SunIcon />
      )}
    </button>
  );
};

export default DarkModeToggle;

themeの使用例

一方、ユーザーに明示的にsystemを選ばせるようなUIにしたい場合はthemeが有効です。

export default function ThemeSelector() {
  const { theme, setTheme, resolvedTheme } = useTheme();

  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <div>
      <label>
        テーマを選択:
        <select
          value={theme}
          onChange={(e) => setTheme(e.target.value)}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
          <option value="system">System</option>
        </select>
      </label>
    </div>
  );
}

Material-UIのドキュメントページはこの作りになっていますね。
https://mui.com/material-ui/getting-started/
(非Webエンジニア向けのサイトではやめた方がいい気はします。systemって言われてもピンと来ないですよね。。)

Hydration Error対策

theme,resolveThemeともにSSRの段階ではundefinedになります。
そのため、対策なしでこれらを使用するとSSR/CSRで描画結果に差異が生じてHydration mismatchが起きてしまいます。
ここまでの説明の中にもあった以下のコードはその対策です。

  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

これにより、クライアントでマウントされるまでは描画が行われないようになるため、SSR/CSRの差がなくなり、エラーを避けることができます。
なお、実際のコードでnullを返すとlayout shiftが問題になると思われます。
スケルトンか何かを返すようにしましょう。

参考

https://github.com/pacocoursey/next-themes

https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-color-scheme

https://developer.chrome.com/docs/devtools/rendering/emulate-css?hl=ja

https://zenn.dev/yoshinoki/articles/next-tailwind-darkmode

Discussion