Reactで複数のContextを組み合わせる方法を完全解説!初心者でもわかるContext APIの応用
生徒
「Context APIってひとつしか使えないんですか?テーマとログイン情報を両方管理したいんですけど。」
先生
「Contextはいくつでも作れますし、複数を同時に使うこともできます。目的ごとに分けて管理するのが実際の開発でも一般的なやり方です。」
生徒
「複数のContextをまとめて使うとき、書き方が複雑にならないか心配です。」
先生
「コツをつかめばシンプルに書けます。実際のコードを見ながら順番に確認していきましょう!」
1. なぜ複数のContextを使うのか?ひとつにまとめてはいけない理由
Context APIを使い始めると、「すべてのデータをひとつのContextにまとめてしまえば楽では?」と思うことがあります。しかし実際の開発では、用途ごとにContextを分けるのが一般的で、その理由を理解しておくことが大切です。
まず、Contextの値が変わると、そのContextを使っているすべてのコンポーネントが再描画されます。再描画(再レンダリング)とは、コンポーネントが最新の状態を反映するために再計算されて画面が更新されることです。すべてのデータをひとつのContextにまとめると、どれかひとつのデータが変わっただけで、そのContextを参照しているすべてのコンポーネントが更新されてしまいます。
また、ひとつのContextにたくさんのデータが入るとコードが読みにくくなり、どのデータがどこで使われているか把握しにくくなります。ログイン情報はユーザーContext、テーマ設定はテーマContext、言語設定は言語Contextというように、役割ごとに分けることでコードが整理されて管理しやすくなります。
2. 複数のContextをProviderで入れ子にする基本的な書き方
複数のContextを使うときは、それぞれのProviderを入れ子にして使います。入れ子(ネスト)とは、コンポーネントの中にさらにコンポーネントを入れることです。内側にあるコンポーネントはすべての親Providerのデータにアクセスできます。
まずはテーマと言語のふたつのContextを作って組み合わせるシンプルな例を見てみましょう。
import React, { createContext, useContext } from "react";
// テーマ用のContextを作成
const ThemeContext = createContext("light");
// 言語用のContextを作成
const LangContext = createContext("ja");
function App() {
return (
// 複数のProviderを入れ子にする
<ThemeContext.Provider value="dark">
<LangContext.Provider value="en">
<MainPage />
</LangContext.Provider>
</ThemeContext.Provider>
);
}
function MainPage() {
// ふたつのContextからそれぞれデータを取り出す
const theme = useContext(ThemeContext);
const lang = useContext(LangContext);
return (
<div style={{ background: theme === "dark" ? "#222" : "#fff", color: theme === "dark" ? "#fff" : "#000", padding: "16px" }}>
<p>テーマ:{theme}</p>
<p>言語:{lang}</p>
</div>
);
}
export default App;
複数のContextを使うときも、useContextを複数回呼び出すだけで、それぞれのContextからデータを取得できます。Providerの順番に決まりはありませんが、外側のProviderのほうが広い範囲に影響を与えます。
3. useStateと組み合わせて複数のContextで状態を管理しよう
実際のアプリでは、Contextのデータが変化する場合がほとんどです。useStateと組み合わせることで、それぞれのContextの状態を独立して管理できます。テーマの切り替えとログイン状態の管理を別々のContextで行う例を見てみましょう。
import React, { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
const AuthContext = createContext(null);
function App() {
const [theme, setTheme] = useState("light");
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
<ControlPanel />
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
function ControlPanel() {
const { theme, setTheme } = useContext(ThemeContext);
const { isLoggedIn, setIsLoggedIn } = useContext(AuthContext);
return (
<div style={{ background: theme === "dark" ? "#333" : "#f0f0f0", padding: "20px" }}>
<p>テーマ:{theme}</p>
<p>ログイン状態:{isLoggedIn ? "ログイン中" : "未ログイン"}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
テーマを切り替える
</button>
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? "ログアウト" : "ログイン"}
</button>
</div>
);
}
export default App;
テーマが変わってもAuthContextを使っているコンポーネントには影響が出ず、ログイン状態が変わってもThemeContextだけを使っているコンポーネントには影響が出ません。Contextを分けておくことでこのような独立性が保たれます。
4. Contextをファイルに分けて複数管理をスッキリさせよう
複数のContextを使うと、App.jsxに定義が集中してコードが長くなりがちです。実際の開発では、それぞれのContextを専用のファイルに切り出して管理するのが一般的です。
ここでは通知メッセージを管理するContextをファイルに切り出す例を紹介します。
// NoticeContext.jsx
import { createContext, useContext, useState } from "react";
export const NoticeContext = createContext(null);
// カスタムフック:useContextをラップして使いやすくする
export function useNotice() {
return useContext(NoticeContext);
}
// Providerコンポーネントをエクスポート
export function NoticeProvider({ children }) {
const [notices, setNotices] = useState([]);
const addNotice = (text) => {
setNotices((prev) => [...prev, text]);
};
const clearNotices = () => {
setNotices([]);
};
return (
<NoticeContext.Provider value={{ notices, addNotice, clearNotices }}>
{children}
</NoticeContext.Provider>
);
}
カスタムフックとは、useContextなどのフックをラップして独自に作った関数のことです。useNotice()と書くだけでContextのデータが取得できるようになるため、毎回useContext(NoticeContext)と長く書く手間が省けます。
このようにContextごとに専用ファイルを作ると、contexts/NoticeContext.jsx、contexts/ThemeContext.jsxのように整理でき、どのファイルが何を管理しているかが一目でわかるようになります。
5. 複数のProviderをひとつにまとめるコンポーネントの作り方
Contextが増えてくると、App.jsxでProviderを何重にも入れ子にするコードが長くなってきます。そこでよく使われるのが、すべてのProviderをひとつのコンポーネントにまとめる方法です。AppProvidersなどの名前でまとめることが多いです。
import React from "react";
import { ThemeProvider } from "./contexts/ThemeContext";
import { NoticeProvider } from "./contexts/NoticeContext";
import { AuthProvider } from "./contexts/AuthContext";
// すべてのProviderをひとつにまとめるコンポーネント
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<NoticeProvider>
{children}
</NoticeProvider>
</ThemeProvider>
</AuthProvider>
);
}
function App() {
return (
// AppProvidersひとつで囲むだけでOK
<AppProviders>
<MainContent />
</AppProviders>
);
}
function MainContent() {
return <p>アプリのメインコンテンツ</p>;
}
export default App;
AppProvidersをひとつ作っておくことで、App.jsxはとてもすっきりした状態を保てます。Providerを追加したいときはAppProvidersに追加するだけなので、変更箇所も一か所にまとまります。アプリが大きくなるほど、この整理の効果を実感できます。
6. 複数のContextを使うときの設計の考え方とよくある失敗
複数のContextを運用するうえで、設計の考え方をあらかじめ知っておくと、後から困ることが減ります。
まず大切なのは、Contextに入れるデータの責任範囲をはっきり決めることです。たとえば「ユーザー情報Context」にはログインユーザーのデータだけを入れ、通知やテーマを混在させないようにします。責任範囲が明確だと、デバッグのときにどのContextを確認すればいいかがすぐわかります。
次によくある失敗として、更新頻度の高いデータと低いデータを同じContextに入れてしまうケースがあります。たとえば1秒ごとに変わるカウンターと、めったに変わらないユーザー名を同じContextに入れると、カウンターが変わるたびにユーザー名を使っているコンポーネントまで再描画されます。更新頻度の違うデータは別のContextに分けることが、パフォーマンスを保つうえで重要です。
また、Context APIで管理する必要がないデータまでContextに入れてしまうことも避けましょう。コンポーネント内だけで使うデータはuseStateで十分です。Context APIはあくまで複数のコンポーネントをまたいで共有する必要があるデータに使うのが正しい使い方です。
7. 複数のContextを活用した実践的なアプリの構成イメージ
実際のWebアプリではどのようにContextを使い分けているのか、よく見られる構成パターンを紹介します。
たとえばECサイト(オンラインショッピングサイト)であれば、次のように役割ごとにContextを分けるのが一般的です。AuthContextでログイン状態とユーザー情報を管理します。CartContextでカートの中身と操作関数を管理します。ThemeContextでダークモードやライトモードの切り替えを管理します。NotificationContextで画面上部に表示するお知らせメッセージを管理します。
これらをAppProvidersにまとめておくことで、App.jsx自体はとてもシンプルな構成を保てます。新しい機能を追加するときも、新しいContextファイルを作ってAppProvidersに追加するだけなので、既存のコードへの影響を最小限に抑えながら機能を拡張できます。
Contextをうまく分けて管理する習慣をつけると、アプリが大きくなってもコードの見通しを保てます。最初から完璧な設計を目指す必要はなく、使いながら少しずつ整理していくのが現実的なアプローチです。