next-themesについてのメモ(`theme`と`resolvedTheme`の使い分け/Hydration Error対策)
ドキュメント読んで微妙にわからなかった部分の備忘録。
TL;DR
-
theme
は「何のテーマを選んでいるか」を返す(light
ordark
orsystem
)
systemに明示的に切り替えたい場合などにはこっち -
resolveTheme
は「実際に何のテーマが使われているか」を返す(light
ordark
)
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を使う場合は以下の設定を追加してあげて、
const config: Config = {
// ...
darkMode: ["selector", ".dark"],
classNameに追加してあげるだけでOKです。簡単!
<p className="text-black dark:text-white">text</p>
ドキュメントはこちら
テーマの切り替え方法
テーマの切り替えにはuseTheme
のsetTheme
を使います。
これに渡したテーマがアプリ全体で共有されることになります。
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
で指定できるあれです。
テーマの使用
現在のテーマはtheme
かresolveTheme
を用いて受け取ることができます。
が、この二つはちょっとした違いがあるので気を付ける必要があります。
まずはドキュメントを見てみましょう。
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
が結果的に何になってるのかまで解決してくれます。
theme
とresolvedTheme
の使い分け
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のドキュメントページはこの作りになっていますね。
(非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が問題になると思われます。
スケルトンか何かを返すようにしましょう。
参考
Discussion