ReactのcreateContextとuseContextの使い方を完全解説!初心者でもわかるContext APIの基本
生徒
「createContextとuseContextって名前が似ていてどう違うのかよくわかりません。」
先生
「createContextはデータを入れる「箱」を作る関数で、useContextはその箱からデータを取り出すフックです。役割がまったく違うので、ひとつずつ覚えていきましょう。」
生徒
「実際にどう書けばいいか、コードで見せてもらえますか?」
先生
「もちろんです。シンプルな例から順番に確認していきましょう!」
1. createContextとuseContextの役割を整理しよう
ReactのContext APIを使うとき、中心になる関数がcreateContextとuseContextの2つです。まずはそれぞれの役割をしっかり理解しておきましょう。
createContextは、データを保管するための「共有の箱」を作る関数です。この箱のことをContextオブジェクトと呼びます。箱を作るときに初期値を設定することができます。作った箱はProviderというコンポーネントを通じて、アプリ内の好きな範囲にデータを提供します。
useContextは、その箱からデータを取り出すためのフックです。フックとは、Reactが用意している特別な関数で、コンポーネントの中でReactの機能を使えるようにするものです。useContextを呼び出すと、近くにあるProviderからデータを受け取ることができます。
ふたつの関係をまとめると、createContextで箱を作り、Providerでデータを入れて、useContextでデータを取り出す、という流れになります。この3ステップがContext APIの基本の型です。
2. createContextの書き方と初期値の設定方法
createContextの基本的な書き方を見ていきましょう。Reactからインポートして呼び出すだけで、Contextオブジェクトが作成できます。
import { createContext } from "react";
// 初期値として空文字を設定したContextを作成
const MessageContext = createContext("");
// 初期値として数値を設定したContextを作成
const CountContext = createContext(0);
// 初期値としてオブジェクトを設定したContextを作成
const UserContext = createContext({ name: "", age: 0 });
createContext()の引数に渡した値が初期値になります。初期値とは、Providerがない状態でuseContextを呼び出したときに返ってくる値です。実際の開発では必ずProviderを使うので、この初期値が使われることはあまりありませんが、TypeScriptなど型の情報を伝える目的で設定することが多いです。
作成したContextオブジェクトはexportして他のファイルから使えるようにしておくのが一般的な書き方です。
3. Providerを使ってデータを提供する方法
createContextで作ったContextオブジェクトには、Providerというコンポーネントが付属しています。このProviderで子コンポーネントを囲み、value属性にデータをセットすることで、囲まれた範囲のすべてのコンポーネントにデータが提供されます。
import React, { createContext } from "react";
// Contextを作成してエクスポート
export const ThemeContext = createContext("light");
function App() {
const currentTheme = "dark";
return (
// ThemeContext.Providerでアプリをラップしてvalueにデータをセット
<ThemeContext.Provider value={currentTheme}>
<div>
<h1>テーマ設定のサンプル</h1>
<ChildComponent />
</div>
</ThemeContext.Provider>
);
}
function ChildComponent() {
return <p>子コンポーネントです</p>;
}
export default App;
valueに渡すデータは文字列だけでなく、数値、真偽値(trueかfalse)、配列、オブジェクトなど何でも設定できます。複数の値をまとめて渡したい場合はオブジェクトにまとめて渡すのが一般的です。
4. useContextでデータを取り出してみよう
Providerで提供されたデータは、useContextフックを使って取り出します。引数にContextオブジェクトを渡すだけで、Providerからセットされたデータを受け取れます。先ほどのテーマ設定の例を完成させてみましょう。
import React, { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Header />
<MainContent />
</ThemeContext.Provider>
);
}
function Header() {
// useContextでThemeContextからデータを取得
const theme = useContext(ThemeContext);
return (
<header style={{ background: theme === "dark" ? "#333" : "#fff", color: theme === "dark" ? "#fff" : "#000", padding: "10px" }}>
<p>現在のテーマ:{theme}</p>
</header>
);
}
function MainContent() {
const theme = useContext(ThemeContext);
return (
<main style={{ background: theme === "dark" ? "#555" : "#f5f5f5", padding: "10px" }}>
<p>メインコンテンツエリアです</p>
</main>
);
}
export default App;
複数のコンポーネントが同じContextからデータを取り出せることが確認できます。propsを使った場合は親から子へ渡す必要がありましたが、useContextを使えば各コンポーネントが直接取得できます。
5. useStateと組み合わせてデータの変更にも対応しよう
Contextはデータを読むだけでなく、useStateと組み合わせることで、データの変更も全コンポーネントに反映できるようになります。useStateとは、コンポーネントの中で変化する値を管理するためのReactフックです。
ProviderのvalueにuseStateの値と更新関数をまとめて渡すことで、子孫コンポーネントからContextの値を書き換えることができます。
import React, { createContext, useContext, useState } from "react";
const LanguageContext = createContext(null);
function App() {
const [language, setLanguage] = useState("日本語");
return (
// valueにオブジェクトとして値と更新関数をまとめて渡す
<LanguageContext.Provider value={{ language, setLanguage }}>
<LanguageSelector />
<Greeting />
</LanguageContext.Provider>
);
}
function LanguageSelector() {
const { setLanguage } = useContext(LanguageContext);
return (
<div>
<button onClick={() => setLanguage("日本語")}>日本語</button>
<button onClick={() => setLanguage("English")}>English</button>
</div>
);
}
function Greeting() {
const { language } = useContext(LanguageContext);
return <p>選択中の言語:{language}</p>;
}
export default App;
このようにuseStateとContextを組み合わせると、状態の読み取りと更新を両方Contextで管理できます。更新関数も一緒にvalueに含めるのがポイントです。
6. Contextの定義を別ファイルに切り出す書き方
実際のアプリ開発では、Contextの作成とProviderコンポーネントを専用ファイルに切り出して管理するのが主流のやり方です。ファイルを分けることで、コードの見通しがよくなり、チームでの開発でも扱いやすくなります。
たとえばUserContext.jsxというファイルを作り、そこにContextとProviderをまとめて定義します。
// UserContext.jsx
import { createContext, useContext, useState } from "react";
// ContextをエクスポートしてどこからでもuseContextで使えるようにする
export const UserContext = createContext(null);
// カスタムフック:useContextをラップして使いやすくする
export function useUser() {
return useContext(UserContext);
}
// Providerコンポーネントをエクスポート
export function UserProvider({ children }) {
const [user, setUser] = useState({ name: "ゲスト", isLoggedIn: false });
const login = (name) => {
setUser({ name: name, isLoggedIn: true });
};
const logout = () => {
setUser({ name: "ゲスト", isLoggedIn: false });
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
このファイルを使うAppコンポーネントは次のように書きます。
// App.jsx
import React from "react";
import { UserProvider, useUser } from "./UserContext";
function UserStatus() {
const { user, login, logout } = useUser();
return (
<div>
<p>{user.isLoggedIn ? user.name + "さんがログイン中" : "ログアウト中"}</p>
{user.isLoggedIn ? (
<button onClick={logout}>ログアウト</button>
) : (
<button onClick={() => login("田中")}>田中さんでログイン</button>
)}
</div>
);
}
function App() {
return (
<UserProvider>
<h1>ログイン状態の管理</h1>
<UserStatus />
</UserProvider>
);
}
export default App;
コードの中でuseUser()というカスタムフックを作っているのがポイントです。カスタムフックとは、useContextなどのフックをラップして、より使いやすい形にした独自の関数のことです。毎回useContext(UserContext)と書く代わりにuseUser()と短く書けるようになります。
7. useContextを使うときによくあるエラーと対処法
useContextを使い始めたころにつまずきやすいポイントをいくつか紹介します。知っておくと、エラーが出たときにすぐ原因を特定できます。
まず多いのが、Providerの外でuseContextを使ってしまうケースです。useContextはProviderで囲まれた範囲の内側にあるコンポーネントでしか正しく動作しません。Providerの外で呼び出した場合、createContextで指定した初期値が返ってきます。初期値をnullにしている場合はエラーになることもあるため注意が必要です。
次に、インポートを忘れてしまうケースもよくあります。useContextはReactからインポートが必要です。また、createContextで作ったContextオブジェクトをインポートし忘れることもあるため、ファイルの先頭のインポート文を確認する習慣をつけましょう。
また、Providerのvalueの書き方のミスも注意が必要です。オブジェクトを渡すときはvalue={{ key: value }}のように二重の波かっこになります。外側の波かっこはJSXの式を書くためのもので、内側の波かっこがオブジェクトのリテラルです。一重にしてしまうと文法エラーになります。
エラーが出たときは、まずProviderで正しく囲まれているか、インポートは正しいか、valueの書き方は合っているかの3点を確認するとほとんどの場合解決できます。