初心者向けReact createContext入門:Hooksで学ぶ状態管理
Reactアプリケーション開発において、「状態管理」は避けて通れない重要なテーマです。アプリケーションが複雑になるにつれて、データの受け渡しや共有が課題となり、コードが読みにくくなったり、保守が難しくなったりすることがよくあります。
この記事では、Reactに標準で備わっている強力な機能であるContext APIと、React Hooksの中でも特にContextの利用に不可欠なuseContext
フックを組み合わせることで、どのように効率的かつクリーンに状態管理を行うかを、初心者の方にも分かりやすく、詳細に解説していきます。
約5000語のボリュームで、理論から具体的なコード例、そして適切な使い方や注意点まで、Context APIを使った状態管理の全てを網羅することを目指します。この記事を読み終える頃には、あなたのReactアプリケーションの状態管理スキルが一段と向上しているはずです。
さあ、Context APIとuseContext
フックの世界へ飛び込み込みましょう!
1. なぜ状態管理が必要なのか? 〜Props Drillingの問題提起〜
Reactでコンポーネント間でデータを受け渡す最も基本的な方法は、「Props」を使うことです。親コンポーネントから子コンポーネントへ、必要なデータをPropsとして渡していきます。これはシンプルで分かりやすい方法ですが、アプリケーションが大きくなるにつれて、ある問題に直面することがあります。
Props Drillingとは?
例えば、アプリケーションの最上位に近いコンポーネント(親)が持っているデータを、深くネストされた子コンポーネント(孫、ひ孫など)で使いたい場合を考えてみましょう。
データは親コンポーネントから直接「ひ孫」コンポーネントへは渡せません。親コンポーネントから子コンポーネントへPropsとして渡し、その子コンポーネントは自身ではそのデータを使わないにもかかわらず、さらに孫コンポーネントへPropsとして渡し、その孫コンポーネントも同様に…というように、データが必要なコンポーネントにたどり着くまで、中間にあるコンポーネントがひたすらPropsをリレーしていく必要が出てきます。
この現象を「Props Drilling(プロップス・ドリリング)」と呼びます。「ドリル」という言葉が示すように、データを下の階層へ掘り進めていくイメージです。
Props Drillingの何が問題か?
- コードの読みにくさ: データが必要ない中間コンポーネントが、大量のPropsを受け取り、そのまま下に渡すだけのコードが増えます。これにより、コンポーネントのPropsのリストが長くなり、そのコンポーネントが「本当に何をしているのか」が分かりにくくなります。
- 保守性の低下: Props Drillingが発生している場合、データの形や名前が変更されると、そのデータを受け渡している全ての中間コンポーネントのコードを修正する必要が出てきます。これは、アプリケーション全体の変更が非常に大変になることを意味します。
- コンポーネントの再利用性の低下: 特定のProps Drillingが発生しているコンポーネントは、そのPropsが提供される特定の階層や構造でしか機能しない可能性が高くなります。他の場所で再利用しようとすると、必要なPropsをうまく渡せない、といった問題が生じやすくなります。
状態管理の必要性
Props Drillingの問題を解決し、アプリケーション全体で共有したい状態(データ)を効率的に管理するために、「状態管理」の考え方や仕組みが必要になります。
状態管理の目的は、アプリケーションのどこからでも特定の状態にアクセスし、必要に応じて更新できるような仕組みを提供することです。これにより、Props Drillingを回避し、コンポーネント間のデータの流れをシンプルに保つことができます。
状態管理ライブラリとしては、Redux, Recoil, Zustandなど様々な選択肢がありますが、Reactには標準機能としてContext APIが備わっています。小規模から中規模のアプリケーションであれば、Context APIだけで十分な状態管理を実現できる場合が多く、外部ライブラリを追加するオーバーヘッドを避けることができます。
そして、React v16.8で導入されたHooksは、Context APIの利用をさらにシンプルかつ直感的にしました。特にuseContext
フックは、関数コンポーネント内で簡単にContextの値を利用するための強力なツールです。
この記事では、このContext APIとuseContext
フックを組み合わせた状態管理に焦点を当てて解説していきます。
2. Context APIの基本を学ぶ
Context APIは、Reactコンポーネントツリー内で値を「提供(Provide)」し、遠く離れたコンポーネントからでもその値を「利用(Consume)」できるようにするための仕組みです。これにより、明示的にPropsとして渡すことなく、コンポーネント間でデータを共有できます。
Context APIを使う上で、主に以下の3つの要素を理解する必要があります。
React.createContext
: Contextオブジェクトを作成します。Context.Provider
: Contextオブジェクトの.Provider
プロパティです。Contextの値を提供するコンポーネントです。Context.Consumer
: Contextオブジェクトの.Consumer
プロパティです。Contextの値を利用するコンポーネントです。(これはクラスコンポーネントやHooks以前の関数コンポーネントで主に使われました。Hooksを使う場合はuseContext
を使います。)
今回はHooks(useContext
)を中心に解説するため、Context.Consumer
については触れませんが、基本的な概念として知っておくとContext APIの歴史や進化を理解する上で役立ちます。
2.1. React.createContext
とは?
React.createContext
関数は、Contextオブジェクトを作成するために使います。
“`javascript
import React from ‘react’;
const MyContext = React.createContext(defaultValue);
“`
React.createContext()
を呼び出すと、新しいContextオブジェクトが作成されます。- 作成されたContextオブジェクトには、
.Provider
と.Consumer
というプロパティが含まれます。 createContext
関数に渡される引数defaultValue
は、Providerが提供されない場合にConsumer(またはuseContext
)が受け取る「デフォルト値」です。これは主にテスト時や、Providerが存在しない場合にフォールバックとして使用されます。Providerが存在する場合は、Providerが提供する値が優先されます。デフォルト値は、データ構造のヒントとしても役立ちます。
例:テーマの色を共有するためのContextを作成する。
“`javascript
// ThemeContext.js
import React from ‘react’;
// デフォルト値としてライトテーマの色を指定
const ThemeContext = React.createContext(‘light’);
export default ThemeContext;
“`
このThemeContext
オブジェクトを使って、値を「提供」したり「利用」したりします。
2.2. Context.Provider
コンポーネントとは?
Context.Provider
は、作成したContextオブジェクトに含まれるプロパティの一つで、Contextの値を下位のコンポーネントツリーに提供するために使用します。
“`jsx
import ThemeContext from ‘./ThemeContext’;
function App() {
const theme = ‘dark’; // ここで提供したい値を定義
return (
{/ このProviderの子コンポーネントツリー全体で ‘theme’ の値を利用可能になる /}
);
}
“`
Context.Provider
コンポーネントは、value
という特別なPropsを受け取ります。- この
value
Propに渡された値が、このProviderコンポーネント以下の子コンポーネントツリー全体で利用可能になります。 - 子コンポーネント(孫、ひ孫を含む)は、後述する
useContext
フックを使うことで、このvalue
として提供された値にアクセスできます。 - 一つのProviderは、一つのContextに対してのみ値を設定できます。複数の種類の値を共有したい場合は、複数のContextを作成するか、オブジェクトとして一つのContextで複数の値をまとめて提供します。
Providerコンポーネントをアプリケーションツリーのどこに配置するかは重要です。通常、共有したいデータが利用される可能性のある最も上位のコンポーネントに配置します。アプリケーション全体で共有したい場合は、ルートコンポーネント(例: App
コンポーネント)に近い場所に配置することが多いです。
2.3. useContext
Hookとは? (Hooksでの利用)
useContext
は、関数コンポーネント内でContextの値を利用するためのReact Hookです。Context APIをHooksで使う場合、これが主役となります。
“`javascript
import React, { useContext } from ‘react’;
import ThemeContext from ‘./ThemeContext’;
function ThemedButton() {
// ThemeContext から現在のテーマの値を取得
const theme = useContext(ThemeContext);
// 取得したテーマの値を使ってボタンのスタイルを決定
const buttonStyle = {
backgroundColor: theme === ‘dark’ ? ‘black’ : ‘white’,
color: theme === ‘dark’ ? ‘white’ : ‘black’,
padding: ’10px’,
border: ‘none’,
cursor: ‘pointer’,
};
return (
);
}
“`
useContext
フックは、引数としてContextオブジェクト(React.createContext
で作成したもの)を受け取ります。useContext(MyContext)
を呼び出すと、Reactはコンポーネントツリーを遡って、最も近いMyContext.Provider
を探します。- 見つかったProviderの
value
Propの値が返されます。 - もしProviderが見つからない場合は、
React.createContext
に指定したデフォルト値が返されます。 - Contextの値が更新されると、そのContextを
useContext
で利用している全てのコンポーネントが再レンダリングされます。
useContext
を使うことで、Props Drillingで何層もPropsを渡す必要がなくなり、どの階層にあるコンポーネントからでもContextの値を直接取得できるようになります。これは、コードの見通しを良くし、保守性を高める上で非常に強力です。
3. Context APIを使った状態管理の実装:簡単な例(テーマ切り替え)
ここからは、Context APIとuseContext
Hookを使って、実際にアプリケーションの状態(ここではUIテーマ)を管理する簡単な例を実装しながら学びましょう。
実装する機能:
アプリケーション全体でUIテーマ(ライトモードまたはダークモード)を共有し、テーマに応じてボタンの色が変わるようにします。さらに、テーマを切り替えるボタンも設置します。
ステップ1: テーマContextを作成する
まず、テーマの値を保持するためのContextオブジェクトを作成します。
src/contexts/ThemeContext.js
:
“`javascript
import React from ‘react’;
// ThemeContextオブジェクトを作成
// デフォルト値は ‘light’ とする
const ThemeContext = React.createContext(‘light’);
export default ThemeContext;
“`
Contextファイルは、src/contexts
のようなディレクトリにまとめておくと管理しやすくなります。
ステップ2: テーマContextのProviderを設定する
アプリケーションのルートに近い場所で、テーマContextのProviderを設定し、値を供給します。ここでは、App
コンポーネントで現在のテーマ状態を持ち、それをProviderに渡すようにします。
src/App.js
:
“`jsx
import React, { useState } from ‘react’;
import ThemeContext from ‘./contexts/ThemeContext’;
import ThemedButton from ‘./components/ThemedButton’;
import ThemeToggleButton from ‘./components/ThemeToggleButton’;
import ‘./App.css’; // 後でスタイルを追加するかもしれません
function App() {
// 現在のテーマを状態として持つ(初期値は ‘light’)
const [theme, setTheme] = useState(‘light’);
// テーマを切り替える関数
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === ‘light’ ? ‘dark’ : ‘light’));
};
return (
// ThemeContext.Providerで、下位コンポーネントに ‘theme’ の値を供給
// さらに、テーマを切り替える関数 ‘toggleTheme’ も一緒に供給する
// Contextは一つの値しか持てないが、オブジェクトにまとめれば複数の値を供給できる
Context API + Hooks テーマ切り替え例
{/ 他のコンポーネントもThemeContextの値を利用可能 /}
この文章もテーマに応じて色が変わるかもしれません。
);
}
export default App;
“`
useState('light')
で、現在のテーマ状態を管理します。初期値は'light'
です。toggleTheme
関数は、現在のテーマ状態を反転させる(’light’なら’dark’に、’dark’なら’light’に)ロジックです。この関数もContextで共有することで、子コンポーネントからテーマを更新できるようになります。ThemeContext.Provider
コンポーネントに、value
Propsとして{ theme: theme, toggleTheme: toggleTheme }
というオブジェクトを渡しています。Contextでは、文字列、数値、オブジェクト、配列、関数など、あらゆる種類の値を共有できます。ここでは、現在のテーマの値(theme
)と、テーマを更新するための関数(toggleTheme
)の両方をまとめてオブジェクトとして提供しています。ThemeContext.Provider
でラップされた<div className={'App theme-${theme}'}>
とその子コンポーネント全てが、このProviderが提供するvalue
にアクセスできるようになります。
ステップ3: Contextの値をHooks (useContext
) で利用する
次に、Contextで提供されたテーマの値や、テーマ切り替え関数を子コンポーネントで利用します。
src/components/ThemedButton.js
:
“`jsx
import React, { useContext } from ‘react’;
import ThemeContext from ‘../contexts/ThemeContext’;
function ThemedButton() {
// useContext Hookを使って ThemeContext から値を取得
// Providerで提供されたオブジェクト { theme: ‘…’, toggleTheme: ‘…’ } が取得できる
const context = useContext(ThemeContext);
// 取得したコンテキストオブジェクトから theme の値を取り出す
const theme = context.theme;
// テーマに応じたスタイル
const buttonStyle = {
backgroundColor: theme === ‘dark’ ? ‘#333’ : ‘#eee’,
color: theme === ‘dark’ ? ‘#eee’ : ‘#333′,
padding: ’10px 20px’,
border: ‘1px solid #ccc’,
borderRadius: ‘5px’,
cursor: ‘pointer’,
margin: ’10px’,
};
console.log(ThemedButton rendering with theme: ${theme}
); // 確認用ログ
return (
);
}
export default ThemedButton;
“`
import { useContext } from 'react';
とimport ThemeContext from '../contexts/ThemeContext';
が必要です。const context = useContext(ThemeContext);
の一行で、Providerから提供されたオブジェクト{ theme: theme, toggleTheme: toggleTheme }
を取得できます。- 取得した
context
オブジェクトから、必要な値であるtheme
を取り出して利用しています。 - この
ThemedButton
コンポーネントが、App
コンポーネントからPropsを介さずに、直接テーマの値にアクセスできていることに注目してください。
ステップ4: Contextの値を更新する(テーマ切り替えボタン)
Providerが提供した「状態を更新する関数」を利用して、Contextの値を変更するコンポーネントを作成します。
src/components/ThemeToggleButton.js
:
“`jsx
import React, { useContext } from ‘react’;
import ThemeContext from ‘../contexts/ThemeContext’;
function ThemeToggleButton() {
// useContext Hookを使って ThemeContext から値を取得
// Providerで提供されたオブジェクト { theme: ‘…’, toggleTheme: ‘…’ } が取得できる
const context = useContext(ThemeContext);
// 取得したコンテキストオブジェクトから toggleTheme 関数を取り出す
const toggleTheme = context.toggleTheme;
return (
);
}
export default ThemeToggleButton;
“`
- ここでも
useContext(ThemeContext)
を使ってContextの値(オブジェクト)を取得します。 - 取得したオブジェクトから
toggleTheme
関数を取り出します。 - このボタンがクリックされると
toggleTheme
関数が実行されます。 toggleTheme
関数はApp.js
で定義されており、App
コンポーネントの状態(theme
)を更新します。App
コンポーネントのtheme
状態が更新されると、ThemeContext.Provider
のvalue
Propの値が変わります。- Providerの
value
が変わると、そのContextをuseContext
で利用している全てのコンポーネント(ThemedButton
やThemeToggleButton
自身)が再レンダリングされ、最新のテーマが反映されます。
ステップ5: アプリケーションを実行する
これらのファイルを保存し、アプリケーションを実行してみましょう (npm start
または yarn start
)。
画面に「Context API + Hooks テーマ切り替え例」という見出し、「テーマを切り替え」ボタン、そして「私はテーマボタンです」というボタンが表示されるはずです。
初期状態ではテーマは「light」です。
「テーマを切り替え」ボタンをクリックしてみてください。
- ボタンのラベルが「テーマを切り替え(現在:dark)」に変わります。
- 「私はテーマボタンです(lightモード)」だったボタンが、「私はテーマボタンです(darkモード)」に変わり、背景色と文字色が反転します。
これは、ThemeToggleButton
がContextから取得したtoggleTheme
関数を呼び出し、それがApp
コンポーネントの状態を更新し、その更新された状態がContextを通じてThemedButton
に伝播し、再レンダリングを引き起こした結果です。Props Drillingなしに、深い階層にあるかもしれないコンポーネントの状態を、別の深い階層にあるコンポーネントから更新できていることが分かります。
また、必要に応じてsrc/App.css
に以下のようなスタイルを追加すると、App
コンポーネント全体やPタグの文字色などもテーマに応じて変化させることができます。
src/App.css
:
“`css
/ 省略 /
.App.theme-light {
background-color: #fff;
color: #333;
}
.App.theme-dark {
background-color: #333;
color: #eee;
}
“`
これで、Context APIとHooksを使った基本的な状態管理(値の共有と更新)のサイクルを体験できました。
4. もう少し複雑な例:ユーザー情報の管理
テーマ切り替えはシンプルな例でしたが、Context APIはより複雑なデータ構造や、複数の関連する状態を管理するためにも利用できます。ここでは、ユーザー情報(名前、ログイン状態など)をContextで管理し、表示と更新を行う例を考えてみましょう。
実装する機能:
ユーザー情報(name
, isLoggedIn
)をContextで管理し、ログイン状態によって表示を切り替えたり、ユーザー名をフォームで更新したりできるようにします。
ステップ1: ユーザーContextを作成する
ユーザー情報を保持するためのContextオブジェクトを作成します。今回は、ユーザー情報オブジェクトと、それを更新するための関数をまとめて提供することを想定します。
src/contexts/UserContext.js
:
“`javascript
import React from ‘react’;
// UserContextオブジェクトを作成
// デフォルト値は、空のユーザー情報とダミーの更新関数
const UserContext = React.createContext({
user: { name: ”, isLoggedIn: false },
setUser: () => {}, // デフォルトのsetUser関数は何もしない
});
export default UserContext;
“`
デフォルト値にダミーの関数を含めるのは、useContext
で取得した際に.setUser
のようなプロパティが存在しない場合にエラーになるのを防ぐためです。これにより、Providerが存在しない場合でも安全にContextのプロパティにアクセスできます。
ステップ2: ユーザーContextのProviderを設定する
App
コンポーネント、あるいはユーザー情報がアプリケーション全体で必要となるような最も上位のコンポーネントで、ユーザー情報の状態を持ち、Providerを通じて提供します。
src/App.js
(UserContextを追加):
“`jsx
import React, { useState } from ‘react’;
import ThemeContext from ‘./contexts/ThemeContext’; // 前の例のContextも残しておく
import UserContext from ‘./contexts/UserContext’; // 新しいContext
import ThemedButton from ‘./components/ThemedButton’;
import ThemeToggleButton from ‘./components/ThemeToggleButton’;
import UserInfoDisplay from ‘./components/UserInfoDisplay’; // 新しいコンポーネント
import UserEditForm from ‘./components/UserEditForm’; // 新しいコンポーネント
import ‘./App.css’;
function App() {
// テーマの状態管理 (前の例)
const [theme, setTheme] = useState(‘light’);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === ‘light’ ? ‘dark’ : ‘light’));
};
// ユーザー情報の状態管理
const [user, setUser] = useState({ name: ‘ゲスト’, isLoggedIn: false });
// ユーザー情報を更新する関数 (Contextを通じて提供する)
const updateUserName = (newName) => {
setUser(prevUser => ({ …prevUser, name: newName, isLoggedIn: true })); // 名前更新時はログイン状態もtrueに
};
const login = () => {
setUser(prevUser => ({ …prevUser, isLoggedIn: true }));
};
const logout = () => {
setUser(prevUser => ({ …prevUser, isLoggedIn: false }));
};
return (
// ThemeContext Provider (前の例)
{/ UserContext Provider /}
{/ value には、ユーザー情報オブジェクトと、更新関数をまとめてオブジェクトとして渡す /}
<div className={`App theme-${theme}`}>
<h1>Context API + Hooks ユーザー情報管理例</h1>
{/* テーマ関連コンポーネント (前の例) */}
<h2>テーマ設定</h2>
<ThemeToggleButton />
<ThemedButton />
<hr /> {/* 区切り線 */}
{/* ユーザー情報関連コンポーネント */}
<h2>ユーザー情報</h2>
<UserInfoDisplay /> {/* ユーザー情報を表示するコンポーネント */}
{/* ログインしている場合のみフォームを表示 */}
{user.isLoggedIn ? <UserEditForm /> : (
<button onClick={login}>ログイン</button>
)}
{user.isLoggedIn && <button onClick={logout} style={{ marginLeft: '10px' }}>ログアウト</button>}
</div>
</UserContext.Provider>
</ThemeContext.Provider>
// Context Providerはネストできる
);
}
export default App;
“`
useState
でuser
という状態を持ち、初期値として{ name: 'ゲスト', isLoggedIn: false }
を設定します。updateUserName
,login
,logout
といったユーザー情報を更新するための関数を定義します。これらの関数は、Contextを通じて子コンポーネントから呼び出されることになります。UserContext.Provider
のvalue
には、現在のuser
オブジェクトと、これらの更新関数をまとめたオブジェクト{ user: user, updateUserName: updateUserName, login: login, logout: logout }
を渡しています。- 複数のContextを使用する場合、このようにProviderをネスト(入れ子)にすることができます。
ステップ3: Contextの値をHooks (useContext
) で利用する(表示側)
ユーザー情報を表示するコンポーネントを作成します。このコンポーネントは、Contextからユーザー情報オブジェクトを取得し、表示します。
src/components/UserInfoDisplay.js
:
“`jsx
import React, { useContext } from ‘react’;
import UserContext from ‘../contexts/UserContext’;
function UserInfoDisplay() {
// UserContext から値を取得
const { user } = useContext(UserContext); // 分割代入で user プロパティだけを取得
console.log(UserInfoDisplay rendering. User:
, user); // 確認用ログ
return (
ユーザー名: {user.name}
ログイン状態: {user.isLoggedIn ? ‘ログイン中’ : ‘ログアウト中’}
);
}
export default UserInfoDisplay;
“`
useContext(UserContext)
で、Providerから提供されたオブジェクト{ user: user, updateUserName: ..., login: ..., logout: ... }
を取得します。- 取得したオブジェクトから、必要な
user
プロパティを分割代入(const { user } = ...
)で取り出しています。これは、Contextオブジェクト全体ではなく、特定のプロパティだけを使いたい場合によく使われるパターンです。 - 取得した
user
オブジェクトのプロパティ(name
,isLoggedIn
)を表示しています。
ステップ4: Contextの値を更新する(フォーム側)
ユーザー名を更新するためのフォームコンポーネントを作成します。このコンポーネントは、Contextからユーザー情報と更新関数を取得し、フォームの値として表示し、変更をContextに反映させます。
src/components/UserEditForm.js
:
“`jsx
import React, { useContext, useState, useEffect } from ‘react’;
import UserContext from ‘../contexts/UserContext’;
function UserEditForm() {
// UserContext から user オブジェクトと updateUserName 関数を取得
const { user, updateUserName } = useContext(UserContext);
// フォーム入力用のローカルステート
const [userNameInput, setUserNameInput] = useState(user.name);
// Contextのuser.nameが変更されたら、入力フィールドの値を更新する
// これは、ログイン/ログアウトなどで Context の user オブジェクト自体が変わった場合に
// フォームに表示される初期値を最新の状態に保つため
useEffect(() => {
setUserNameInput(user.name);
}, [user.name]); // user.name が依存配列
// 入力フィールドの値が変更されたときのハンドラ
const handleInputChange = (event) => {
setUserNameInput(event.target.value);
};
// フォーム送信時のハンドラ
const handleSubmit = (event) => {
event.preventDefault(); // ページの再読み込みを防ぐ
// Contextから取得した updateUserName 関数を呼び出し、ユーザー名を更新
updateUserName(userNameInput);
alert(‘ユーザー名を更新しました!’); // 更新完了のフィードバック
};
console.log(UserEditForm rendering. user.name: ${user.name}, userNameInput: ${userNameInput}
); // 確認用ログ
// ログインしていない場合はフォームを表示しない
// App.jsで既に条件分岐しているが、ここでもContextの値を見て表示/非表示を制御することも可能
if (!user.isLoggedIn) {
return null;
}
return (
);
}
export default UserEditForm;
“`
useContext(UserContext)
でContextの値を取得し、user
オブジェクトとupdateUserName
関数を分割代入で取り出します。- フォームの入力値は、通常、そのコンポーネント自身のローカルステート(
userNameInput
)で管理します。Contextの値を直接入力フィールドの値に使うことも可能ですが、入力中の変更(例えばタイピングの度にContextが更新される)が望ましくない場合が多いため、一時的な入力値はローカルステートで持つのが一般的です。 useEffect
を使って、Contextのuser.name
が変更された場合に、ローカルステートのuserNameInput
も最新の値に同期させています。これは、例えばログイン/ログアウトでユーザーオブジェクト全体が変わった場合などに対応するためです。- フォームが送信されたら、
handleSubmit
関数内でContextから取得したupdateUserName(userNameInput)
を呼び出します。これにより、App
コンポーネントで管理されているユーザー情報状態が更新されます。 - ユーザー情報状態が更新されると、
UserContext.Provider
のvalue
が変更され、このContextをuseContext
で利用している全てのコンポーネント(UserInfoDisplay
やUserEditForm
自身)が再レンダリングされ、最新の情報が表示されるようになります。
ステップ5: アプリケーションを実行する
アプリケーションを実行し、ユーザー情報管理機能を確認しましょう。
初期状態では「ユーザー名: ゲスト」「ログイン状態: ログアウト中」と表示され、「ログイン」ボタンが表示されます。
- ログインボタンをクリック: 「ログイン」ボタンが消え、ユーザー名とログイン状態が表示され、さらにユーザー名変更フォームが表示されます。「ユーザー名: ゲスト」「ログイン状態: ログイン中」と表示されているはずです。これは、Contextの
login
関数が呼び出され、user.isLoggedIn
がtrue
に更新されたためです。 - フォームでユーザー名を変更: フォームに新しい名前を入力し、「ユーザー名を保存」ボタンをクリックしてください。Contextの
updateUserName
関数が呼び出され、user.name
が更新されます。 - 変更の確認:
UserInfoDisplay
コンポーネントに表示されているユーザー名が、入力した新しい名前に即座に更新されることを確認してください。Props Drillingなしに、フォームコンポーネントから遠く離れた表示コンポーネントの状態表示が更新されています。 - ログアウトボタンをクリック: 「ログアウト」ボタンをクリックすると、「ログイン」ボタンが表示され、フォームが消え、「ユーザー名: (前の名前)」「ログイン状態: ログアウト中」と表示されるはずです。これはContextの
logout
関数が呼び出され、user.isLoggedIn
がfalse
に更新されたためです。
このように、Context APIとuseContext
フックを使うことで、アプリケーションの状態を管理し、複数のコンポーネント間で共有・更新する仕組みを構築できます。Props Drillingを回避し、コンポーネントの構造をシンプルに保つことが可能です。
5. Context APIの適切な使い方と注意点
Context APIは強力なツールですが、全ての状態管理のケースに適しているわけではありません。その特性を理解し、適切に使うことが重要です。
5.1. どんな時にContextを使うべきか?
Context APIは、以下の種類の状態管理に特に適しています。
- グローバルな設定: アプリケーション全体で共通して使われる設定情報(例: テーマ設定、言語設定)。
- 認証情報: 現在のユーザー情報やログイン状態。
- ユーザーの好み: UIの表示設定や、アプリケーションの振る舞いを変更するユーザー固有の設定。
- 一部のキャッシュデータ: 頻繁に更新されないが、多くのコンポーネントが必要とするデータ。
これらの情報は、アプリケーションツリーの様々な場所で必要とされる可能性が高く、Contextを使うことでProps Drillingを効果的に回避できます。
5.2. Contextを使うべきでない時
Context APIを使うのが適切でないケースもあります。
- 頻繁に更新される状態: Contextの値が更新されると、そのContextを
useContext
で利用している全てのコンポーネント(たとえ値の特定のプロパティだけを使っている場合でも)がデフォルトで再レンダリングされます。非常に頻繁に更新される値(例: マウス座標、入力中のテキストのリアルタイムバリデーション結果など)をContextに入れると、不要な再レンダリングが多発し、パフォーマンス問題を引き起こす可能性があります。 - 多くの異なる値をまとめて Context に入れる: 一つのContextで非常に多くの独立した値を共有すると、その値のうち一つでも更新された場合に、全てのConsumerが再レンダリングされるトリガーになります。Contextは、論理的に関連性の高い状態をまとめて共有するのに適しています。独立性の高い複数の種類の状態を管理したい場合は、Contextを分割することを検討すべきです。
- コンポーネントツリーの一部分でのみ必要な状態: 特定のコンポーネントとその直下の子コンポーネントだけで共有したい状態であれば、Propsで渡すか、コンポーネント自身の
useState
やuseReducer
で管理するのが最もシンプルです。Contextはツリー全体または広範囲で共有したい場合にメリットが大きいです。
Contextは、あくまでReactの標準機能であり、大規模で複雑な状態管理には限界があります。例えば、非同期処理による状態更新、デバッグ機能、middlewareの導入、状態の正規化などが複雑になってくると、ReduxやRecoil、Zustandなどの専用の状態管理ライブラリの方が適している場合があります。
5.3. Props Drilling vs Context API
Props Drillingは見た目が悪く保守が大変になる問題ですが、データの流れが明示的であるというメリットもあります。「このコンポーネントは何を受け取って動いているのか」がPropsのリストを見れば分かります。
一方、Context APIを使うと、データがどこから供給されているのか(どのProviderが一番近いのか)がコードを追わないと分かりにくくなることがあります。useContext
を使っているコンポーネントは、Providerがすぐ近くにあるのか、あるいはアプリケーションのルートにあるのか、それだけでは判断できません。これは、Contextを乱用した場合にコードの見通しを悪くする要因となり得ます。
結論として:
- 数階層程度のProps Drillingであれば、Contextを導入するメリットよりもPropsの明示性を選ぶ方が良い場合があります。
- 多くのコンポーネントで、深い階層からアクセスする必要があり、かつ更新頻度が高すぎない状態であれば、Context APIはProps Drillingの強力な代替手段となります。
6. パフォーマンスを意識したContext APIの利用
前述の通り、Contextの値が更新されると、そのContextをuseContext
で利用している全てのコンポーネントが再レンダリングされる可能性があります。これはパフォーマンスボトルネックになりうるため、いくつか対策を講じることが重要です。
6.1. Contextの分割
Contextを分割することは、パフォーマンス最適化の基本的な戦略の一つです。
例えば、ユーザー情報とアプリケーション設定という、論理的に異なる2つの状態を管理しているとします。これらを一つのContextオブジェクトにまとめてしまうと、ユーザー情報が更新されただけでも、設定情報を表示しているコンポーネントまで再レンダリングされてしまう可能性があります。
これを避けるために、ユーザー情報用のContextと、設定情報用のContextというように、別々のContextに分割します。
“`javascript
// src/contexts/UserContext.js (ユーザー情報と更新関数)
export const UserContext = React.createContext({ user: { … }, setUser: () => {} });
// src/contexts/SettingsContext.js (設定情報と更新関数)
export const SettingsContext = React.createContext({ settings: { … }, setSettings: () => {} });
“`
そして、Providerもそれぞれ用意し、ネストして配置します。
“`jsx
// src/App.js
import { UserContext } from ‘./contexts/UserContext’;
import { SettingsContext } from ‘./contexts/SettingsContext’;
function App() {
const [user, setUser] = useState(…);
const [settings, setSettings] = useState(…);
return (
{/ ここ以下で UserContext と SettingsContext の両方を利用可能 /}
);
}
“`
このようにContextを分割することで、UserContext
の値が更新されても、SettingsContext
だけをuseContext
で利用しているコンポーネントは再レンダリングされなくなります(Reactの最適化により、Contextの値自体が変わらなければConsumerは再レンダリングされないため)。
6.2. Providerのvalue
オブジェクトの安定性
Providerに渡すvalue
Propが、不要な再レンダリングのトリガーになることがあります。JavaScriptでは、オブジェクトや配列はたとえ中身が同じでも、毎回新しいオブジェクトや配列を作成すると「参照が異なる」と判断されます。
上記のユーザー情報管理の例で、Providerのvalue
に{ user: user, updateUserName: updateUserName, login: login, logout: logout }
というオブジェクトを渡していました。
jsx
<UserContext.Provider value={{ user: user, updateUserName: updateUserName, login: login, logout: logout }}>
{/* ... */}
</UserContext.Provider>
ここで、App
コンポーネントが再レンダリングされるたびに、user
、updateUserName
、login
、logout
の値自体が変わっていなくても、新しい{ ... }
オブジェクトが作成されてしまいます。ReactはProviderのvalue
が変更されたと判断し、Contextを利用している全てのConsumerを再レンダリングします。
もしupdateUserName
, login
, logout
関数がApp
コンポーネント内で定義されており、これらの関数自体は再レンダリングの度に再生成されてしまう場合(これはJavaScriptのデフォルトの振る舞いです)、value
オブジェクトに含まれる関数の参照が変わるため、確実にConsumerの再レンダリングが発生します。
この問題を解決するために、useMemo
とuseCallback
フックを使います。
useMemo
: 値(オブジェクトや配列など)の再計算を防ぎます。依存配列が変更されない限り、前回計算された値を再利用します。useCallback
: 関数の再作成を防ぎます。依存配列が変更されない限り、前回作成された関数を再利用します。
先ほどのApp.js
のUserContext Providerの部分を最適化してみましょう。
“`jsx
import React, { useState, useMemo, useCallback } from ‘react’;
// … 他のimport
function App() {
// … テーマの状態管理
// ユーザー情報の状態管理
const [user, setUser] = useState({ name: ‘ゲスト’, isLoggedIn: false });
// ユーザー情報を更新する関数 – useCallback でメモ化
// 依存配列は setUser のみ(setUserはReactによって安定した参照が保証される)
const updateUserName = useCallback((newName) => {
setUser(prevUser => ({ …prevUser, name: newName, isLoggedIn: true }));
}, [setUser]); // 空の配列でも良い場合が多いが、依存関係を明確に
const login = useCallback(() => {
setUser(prevUser => ({ …prevUser, isLoggedIn: true }));
}, [setUser]);
const logout = useCallback(() => {
setUser(prevUser => ({ …prevUser, isLoggedIn: false }));
}, [setUser]);
// Providerに渡す value オブジェクト – useMemo でメモ化
// 依存配列は user, updateUserName, login, logout
// いずれかが変わらない限り、新しいオブジェクトは作成されない
const userContextValue = useMemo(() => {
return {
user: user,
updateUserName: updateUserName,
login: login,
logout: logout,
};
}, [user, updateUserName, login, logout]);
return (
{/ useMemo で作成した安定したオブジェクトを value に渡す /}
<div className={`App theme-${theme}`}>
<h1>Context API + Hooks ユーザー情報管理例</h1>
{/* ... 残りのコンポーネント */}
</div>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
export default App;
“`
updateUserName
,login
,logout
関数をuseCallback
でラップしました。これにより、App
が再レンダリングされても、これらの関数が依存する値(ここではsetUser
だけ)が変わらない限り、同じ関数参照が使い回されます。- Providerに渡すオブジェクト
{ user: user, updateUserName: ..., ... }
をuseMemo
でラップしました。このオブジェクトは、user
,updateUserName
,login
,logout
のいずれかの値(参照)が変わった場合にのみ新しく作成されます。 useMemo
の依存配列には、オブジェクトのプロパティとして含まれている値 (user
,updateUserName
,login
,logout
) を指定します。
この最適化により、例えばテーマだけが切り替わってApp
コンポーネントが再レンダリングされた場合でも、user
, updateUserName
, login
, logout
の値(参照)は変わらないため、userContextValue
オブジェクトは再作成されません。したがって、UserContext.Provider
のvalue
Propは変更されていないと判断され、UserContext
をuseContext
で利用しているコンポーネントの不要な再レンダリングを抑制できます。
ただし、この最適化はContextの値を利用するコンポーネントの再レンダリングを完全に防ぐわけではありません。あくまで、Providerのvalue
の変更による再レンダリングを防ぐためのものです。Contextの値(例えばuser
オブジェクト)自体が変更された場合は、Contextを利用しているコンポーネントは再レンダリングされます。これは意図された挙動です。
また、useMemo
やuseCallback
はそれ自体に若干のオーバーヘッドがあるため、全てのContext Providerに無差別に適用すべきではありません。プロファイラなどでパフォーマンスボトルネックが確認された場合に、必要に応じて導入を検討するのが現実的です。しかし、オブジェクトや配列、関数をContextで共有する場合、通常はこのようにメモ化しておく方がパフォーマンス上のメリットが得られやすいでしょう。
7. 応用例とさらなる学習
Context APIは、様々な種類の状態管理に利用できます。
- 認証: ユーザーの認証状態、認証トークン、ユーザーオブジェクトなどをContextで管理し、ログイン/ログアウト機能や、ログイン状態に応じた表示切り替えを実装します。
- 国際化 (i18n): 現在の言語設定や、翻訳関数などをContextで提供し、アプリケーションのどこからでもテキストをローカライズできるようにします。
- 設定: アプリケーションの挙動に関する様々な設定(グリッド表示/リスト表示、ソート順など)を管理し、ユーザーの好みを反映させます。
- UIの状態: モーダルやサイドバーが開いているか、といったUIの状態を管理し、異なるコンポーネントからこれらの状態を操作できるようにします(ただし、UIの状態はContextではなく、そのUIを管理するコンポーネントに近い場所で管理する方が適切な場合も多いです)。
Context APIはReactの標準機能として非常に便利ですが、アプリケーションの規模や複雑さによっては、より高度な状態管理ライブラリを検討する必要が出てくるかもしれません。
他の状態管理ライブラリとの比較:
- Redux: 集中型のストア、厳格な変更ルール(Reducer、Action、Store)、豊富なエコシステムが特徴です。大規模で複雑なアプリケーション、特に変更履歴の追跡やデバッグが重要な場合に強力です。しかし、Context APIと比較するとボイラープレートコードが多くなりがちです。
- Recoil: Facebookが開発したライブラリで、Atomという単位で状態を管理し、
useRecoilState
などのHookでアクセスします。Context APIより柔軟で、Reduxより学習コストが低いとされています。Reactに特化しており、Concurrent Modeにも対応しています。 - Zustand: シンプルで軽量なHookベースの状態管理ライブラリです。ボイラープレートが非常に少なく、Context APIのようにProviderでツリーをラップする必要もありません(状態へのアクセスはHook経由)。Context APIのシンプルさと、より高度な機能やパフォーマンスを両立させたい場合に良い選択肢です。
Context APIは、これらのライブラリを学ぶ上での基礎ともなります。なぜなら、これらのライブラリの多くが内部的にContext APIを利用して状態をコンポーネントツリーに伝播させているからです。Context APIをしっかり理解しておけば、他のライブラリの仕組みもスムーズに理解できるようになります。
また、Context APIと他のライブラリを組み合わせて使用することも可能です。例えば、アプリケーション全体で共有する基本的な設定や認証情報はContextで管理し、特定機能の複雑な状態はRecoilやZustandで管理するといった使い分けができます。
8. まとめ
この記事では、Reactにおける状態管理の課題であるProps Drillingから始め、Context APIとHooks(特にuseContext
)を使った状態管理の基本的な仕組み、そして具体的な実装例を通じてその使い方を学びました。
Context APIとuseContext
フックを使うことでできること:
- Reactコンポーネントツリーの異なる階層にあるコンポーネント間で、Propsを経由せずに値を共有できる。
- Providerコンポーネントで共有したい値を定義し、子コンポーネントツリー全体に提供できる。
useContext(MyContext)
Hookを関数コンポーネント内で呼び出すだけで、Providerが提供した値を取得できる。useState
やuseReducer
で状態を管理し、その状態と更新関数をContextで共有することで、どのConsumerからでも状態を更新できるインタラクティブな状態管理が可能になる。- Props Drillingを解消し、コードの見通しや保守性を向上させることができる。
Context APIを使う上での注意点:
- Contextの値が更新されると、そのContextを利用している全てのConsumerが再レンダリングされる可能性がある。
- 頻繁に更新される値や、論理的に関連性の低い値を一つのContextにまとめすぎると、パフォーマンス問題やコードの見通しの悪化につながる可能性がある。
- Providerに渡す
value
オブジェクトの参照が不要に変わらないように、useMemo
やuseCallback
を使った最適化が有効な場合がある。 - 大規模で複雑な状態管理には、Context APIだけでは限界がある場合があり、他の状態管理ライブラリの検討が必要になることがある。
Context APIとuseContext
は、Reactの標準機能でありながら非常に強力で、多くのWebアプリケーション開発において必要不可欠なツールです。Props Drillingに悩んだり、簡単なグローバル状態を管理したいと感じたりしたら、まずContext APIを試してみるのが良いスタート地点になるでしょう。
この記事が、あなたがContext APIとuseContext
フックを使ったReactの状態管理を理解し、実際のアプリケーション開発に応用するための一助となれば幸いです。
Happy Coding!