ReactのContext APIでダークモードを実装!テーマ切り替えの方法を初心者向けに完全解説
生徒
「Reactでダークモードとライトモードをボタンで切り替える機能を作りたいんですが、どうすればいいですか?」
先生
「テーマの切り替えはContext APIで管理するのがとても向いています。テーマ情報はアプリ全体のコンポーネントで使う必要があるので、Contextで一元管理すると便利ですよ。」
生徒
「ページをリロードしてもダークモードを維持する方法もありますか?」
先生
「localStorageと組み合わせれば、ブラウザを閉じても設定を保持できます。順番に実装していきましょう!」
1. ダークモードとは?テーマ切り替え機能の概要を理解しよう
最近のWebサービスやアプリでは、画面を暗い配色に切り替える「ダークモード」が広く使われています。目の疲れを軽減したり、暗い環境での使用を快適にしたりする効果があり、ユーザーに好まれる機能のひとつです。逆に明るい配色の画面を「ライトモード」といいます。
Reactでこのテーマ切り替え機能を実装するとき、テーマの状態(ダークかライトか)はアプリ全体で共有する必要があります。ヘッダー、サイドバー、コンテンツエリア、フッターなど、すべてのコンポーネントがテーマに合わせた色で表示される必要があるためです。
このような「アプリ全体で共有するデータ」の管理にはContext APIが最適です。テーマ用のContextを作り、アプリ全体をProviderで囲んでおくと、どのコンポーネントからでも現在のテーマを取得・変更できるようになります。
2. ThemeContextを作成してテーマ情報を管理しよう
まずはテーマ管理専用のContextファイルを作成します。ここではThemeContext.jsxという名前でファイルを作り、Contextの定義とProviderコンポーネントをまとめます。
// ThemeContext.jsx
import { createContext, useContext, useState } from "react";
// テーマ用のContextを作成
export const ThemeContext = createContext(null);
// カスタムフック:useContextをラップして使いやすくする
export function useTheme() {
return useContext(ThemeContext);
}
// Providerコンポーネント
export function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
const toggleTheme = () => {
setIsDark((prev) => !prev);
};
// テーマに応じた色をオブジェクトにまとめる
const theme = {
isDark,
toggleTheme,
colors: {
background: isDark ? "#1a1a2e" : "#f8f9fa",
surface: isDark ? "#16213e" : "#ffffff",
text: isDark ? "#e0e0e0" : "#212529",
subText: isDark ? "#a0a0a0" : "#6c757d",
border: isDark ? "#444" : "#dee2e6",
},
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
カスタムフックとは、useContextなどのReactフックをラップして独自に作った関数のことです。useTheme()と短く書くだけでテーマのデータを取得できるようになるため、毎回useContext(ThemeContext)と書く手間が省けます。
3. Appコンポーネントにプロバイダーを設定してテーマを適用しよう
作成したThemeProviderをApp.jsxで読み込み、アプリ全体を囲みます。こうすることでアプリ内のすべてのコンポーネントがテーマ情報にアクセスできます。
// App.jsx
import React from "react";
import { ThemeProvider, useTheme } from "./ThemeContext";
function Header() {
const { isDark, toggleTheme, colors } = useTheme();
return (
<header style={{
background: colors.surface,
color: colors.text,
borderBottom: "1px solid " + colors.border,
padding: "12px 20px",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<span style={{ fontWeight: "bold" }}>マイサイト</span>
<button
onClick={toggleTheme}
style={{ background: "transparent", border: "1px solid " + colors.border, color: colors.text, padding: "6px 14px", borderRadius: "6px", cursor: "pointer" }}
>
<i class={isDark ? "bi bi-sun-fill" : "bi bi-moon-fill"}></i>
{isDark ? " ライトモード" : " ダークモード"}
</button>
</header>
);
}
function MainContent() {
const { colors } = useTheme();
return (
<main style={{ background: colors.background, color: colors.text, minHeight: "200px", padding: "20px" }}>
<h2 style={{ color: colors.text }}>コンテンツエリア</h2>
<p style={{ color: colors.subText }}>テーマに合わせて色が変わります。</p>
</main>
);
}
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
</ThemeProvider>
);
}
export default App;
ThemeProviderがHeaderとMainContentの両方を囲んでいるため、どちらのコンポーネントもuseThemeでテーマ情報を取得できています。ボタンを押したときにtoggleThemeが呼ばれ、isDarkの値が切り替わると、Contextを使っているすべてのコンポーネントが自動的に再描画されます。
4. localStorageでダークモードの設定をブラウザに保存しよう
現状ではページをリロードするとダークモードの設定がリセットされてしまいます。localStorage(ローカルストレージ)を使うと、ブラウザにデータを保存して、ページをリロードしても設定を維持できるようになります。localStorageとはブラウザが持つ小さなデータ保存領域のことで、シンプルな文字列のデータを保存しておくことができます。
// ThemeContext.jsx(localStorage対応版)
import { createContext, useContext, useState, useEffect } from "react";
export const ThemeContext = createContext(null);
export function useTheme() {
return useContext(ThemeContext);
}
export function ThemeProvider({ children }) {
// localStorageから保存済みのテーマを読み込む(なければfalse)
const [isDark, setIsDark] = useState(() => {
const saved = localStorage.getItem("theme");
return saved === "dark";
});
const toggleTheme = () => {
setIsDark((prev) => !prev);
};
// isDarkが変わるたびにlocalStorageに保存する
useEffect(() => {
localStorage.setItem("theme", isDark ? "dark" : "light");
}, [isDark]);
const theme = {
isDark,
toggleTheme,
colors: {
background: isDark ? "#1a1a2e" : "#f8f9fa",
surface: isDark ? "#16213e" : "#ffffff",
text: isDark ? "#e0e0e0" : "#212529",
border: isDark ? "#444" : "#dee2e6",
},
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
useStateの引数に関数を渡す書き方(useState(() => {...}))を遅延初期化といいます。コンポーネントが最初に表示されるタイミングだけ初期値の計算が行われるため、localStorageへのアクセスを一度だけに抑えられます。useEffectはisDarkが変わるたびにlocalStorageへの保存処理を実行します。
5. OSのダークモード設定に自動で合わせる方法
スマートフォンやパソコンのOSには、システム全体をダークモードにする設定があります。この設定をReactで検知して、アプリのテーマを自動的に合わせることもできます。
JavaScriptにはwindow.matchMediaというAPIがあり、OSのカラーモード設定を取得できます。APIとは、プログラムから別の機能やサービスを利用するための窓口のようなものです。
import { createContext, useContext, useState, useEffect } from "react";
export const ThemeContext = createContext(null);
export function useTheme() {
return useContext(ThemeContext);
}
export function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(() => {
// localStorageに設定があればそちらを優先
const saved = localStorage.getItem("theme");
if (saved) return saved === "dark";
// なければOSの設定に合わせる
return window.matchMedia("(prefers-color-scheme: dark)").matches;
});
const toggleTheme = () => setIsDark((prev) => !prev);
useEffect(() => {
localStorage.setItem("theme", isDark ? "dark" : "light");
}, [isDark]);
const theme = {
isDark,
toggleTheme,
bg: isDark ? "#1a1a2e" : "#f8f9fa",
text: isDark ? "#e0e0e0" : "#212529",
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
window.matchMedia("(prefers-color-scheme: dark)").matchesがtrueのとき、OSがダークモードに設定されていることを意味します。localStorageに保存済みの設定があればそちらを優先し、なければOSの設定を初期値として使う、という優先順位になっています。
6. カードコンポーネントにテーマを適用してUIを仕上げよう
実際のアプリでは複数のコンポーネントが同じテーマ情報を使います。カードコンポーネントを追加してテーマが正しく適用されることを確認してみましょう。
// App.jsx
import React from "react";
import { ThemeProvider, useTheme } from "./ThemeContext";
function ThemeToggleButton() {
const { isDark, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} class="btn btn-outline-secondary mb-4">
<i class={isDark ? "bi bi-sun-fill" : "bi bi-moon-fill"}></i>
{isDark ? " ライトモードに切り替え" : " ダークモードに切り替え"}
</button>
);
}
function ArticleCard({ title, description }) {
const { colors } = useTheme();
return (
<div style={{
background: colors.surface,
color: colors.text,
border: "1px solid " + colors.border,
borderRadius: "8px",
padding: "16px",
marginBottom: "12px"
}}>
<h3 style={{ color: colors.text, fontSize: "1rem", marginBottom: "8px" }}>{title}</h3>
<p style={{ color: colors.subText, fontSize: "0.9rem", margin: 0 }}>{description}</p>
</div>
);
}
function App() {
return (
<ThemeProvider>
<div style={{ padding: "20px" }}>
<ThemeToggleButton />
<ArticleCard title="React入門" description="Reactの基本的な使い方を学ぼう" />
<ArticleCard title="Context APIの使い方" description="グローバルな状態管理をマスターしよう" />
<ArticleCard title="ダークモードの実装" description="テーマ切り替えをContext APIで管理する" />
</div>
</ThemeProvider>
);
}
export default App;
このように、Context APIでテーマを管理することで新しいコンポーネントを追加するたびにpropsを渡す必要がなく、useTheme()を呼び出すだけでテーマに対応できます。アプリが大きくなっても、テーマの管理場所はThemeContext.jsxのひとつだけです。
7. テーマ切り替えをさらに使いやすくするための工夫
ここまでで基本的なダークモードの実装は完成しました。実際の開発でよく見られる追加の工夫もいくつか紹介します。
まず、CSSカスタムプロパティ(CSS変数)との組み合わせです。CSSカスタムプロパティとは、CSSの中で変数のように使える値のことです。Reactのstyle属性でインラインスタイルを書く代わりに、テーマが変わったときにCSSカスタムプロパティの値を書き換えることで、CSSファイルでスタイルを管理しながらテーマ切り替えができます。コンポーネントの数が多くなるほど、この方法のほうがスタイルを一か所にまとめられるメリットが大きくなります。
次に、切り替えアニメーションを追加する方法です。CSSのtransitionプロパティを使うと、テーマが切り替わるときに色がなめらかに変化するアニメーションを付けられます。transition: background 0.3s, color 0.3sのようにスタイルに追加するだけで、ぱっと切り替わるより自然な印象になります。
また、テーマの種類を増やす方法もあります。ダークとライトの2択だけでなく、「セピア」「ハイコントラスト」など複数のテーマを選べるようにすることも可能です。isDarkというboolean値(真偽値)の代わりに、テーマ名を文字列で管理するよう変更するだけで対応できます。Context APIの設計をしっかり作っておくと、こうした拡張が比較的スムーズに行えます。