1. はじめに
今回は「挫折しないReactの教科書」の第5章を学習しました。
テーマはグローバルステートとContext APIです。
propsでのデータ共有の限界と、それを解決するContext APIの使い方を整理しておきます。
2. propsのバケツリレーとその問題点
まずはpropsを使ったコンポーネント間のデータ共有の課題を学びました。
2.1 propsでのstate共有
propsを使うと、親コンポーネントのstateを子コンポーネントに渡すことができます。
以下はその基本的な例です。
親から子へstateをpropsで渡すコード:
// 親コンポーネント
function App() {
// stateで選択された色を管理
const [selectedColor, setSelectedColor] = useState("blue");
return (
// ColorDisplayコンポーネントにcolorという名前でselectedColorを渡す
<ColorDisplay color={selectedColor} />
);
}
// 子コンポーネント
function ColorDisplay(props) {
// propsからcolorを取り出す
const { color } = props;
return <h1 style={{ color: color }}>選択された色: {color}</h1>;
}
1〜2層程度の構造であれば、この方法で問題ありません。
しかし、コンポーネントの階層が深くなると、問題が生じてくるみたいです。
2.2 バケツリレーのデメリット
大規模なアプリケーションでは、App → ComponentA → ComponentB → ComponentC のように5層以上になることがよくあるようです。
この深い階層でpropsを受け渡し続けると、以下の3つのデメリットが生じます。
propsが増えてコンポーネントの目的がわからなくなるという問題があります。
中間コンポーネントが本来の役割とは関係ないpropsを大量に持つことになり、そのコンポーネントが何をするものなのかが不明確になるみたいです。
コードが複雑になり保守性が悪化します。
propsの名前が変更されるたびに、経由するすべてのコンポーネントを修正する必要があり、どのデータがどこで使われているかの把握が難しくなるようです。
無駄な再レンダリングが発生するという問題もあります。
propsでデータが更新されると、そのデータを使わない中間コンポーネントも含めて、すべてのコンポーネントで処理の再実行が発生する可能性があり、パフォーマンスの低下につながるみたいです。
2.3 propsバケツリレーを体験するサンプル
この問題を実感するために、入力されたテキストを複数のコンポーネントで表示するサンプルを作りました。
stateを管理するApp.jsxの全体コードです:
import { useState } from 'react';
// コンポーネントをインポート
import WrapperA from './WrapperA';
import ComponentB from './ComponentB';
function App() {
// カウントとテキストのstateを定義
const [count, setCount] = useState(0);
const [inputText, setInputText] = useState('');
return (
<>
<div>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
{/* WrapperAコンポーネントにinputTextをpropsで渡す */}
<WrapperA inputText={inputText} />
{/* ComponentBコンポーネントにinputTextをpropsで渡す */}
<ComponentB inputText={inputText} />
</div>
</>
);
}
export default App;
中間コンポーネントであるWrapperA.jsxのコードです。
inputTextを自分では使わないのに、ComponentAへ渡すためだけにpropsとして受け取っています:
import ComponentA from './ComponentA';
function WrapperA({ inputText }) {
// propsでinputTextを受け取る(WrapperA自体は使用しない)
return (
<div style={{ border: '1px solid #000', marginTop: '10px' }}>
<p>WrapperA</p>
{/* ComponentAコンポーネントにさらにinputTextを渡す */}
<ComponentA inputText={inputText} />
</div>
);
}
export default WrapperA;
実際にinputTextを使用するComponentA.jsxとComponentB.jsxのコードです:
// ComponentA.jsx
function ComponentA({ inputText }) {
// propsでinputTextを受け取る
return (
<div>
{/* やっとここでテキスト情報を使用 */}
<p>ComponentAで表示中: {inputText}</p>
</div>
);
}
export default ComponentA;
// ComponentB.jsx
function ComponentB({ inputText }) {
// propsでinputTextを受け取る
return (
<div>
{/* ここでもテキスト情報を使用 */}
<p>ComponentBで表示中: {inputText}</p>
</div>
);
}
export default ComponentB;
WrapperAはinputTextを自分では使わないにもかかわらず、ComponentAへ渡すためだけに受け取っている点がバケツリレーの典型例だと理解しました。
3. グローバルステートとContext API
propsバケツリレーの問題を解決するために、グローバルステートとContext APIを学びました。
3.1 グローバルステートとは
グローバルステートとは、アプリケーション全体で共有されるstateのことです。
グローバルステートを使うと、必要なコンポーネントが直接データにアクセスでき、中間コンポーネントを経由する必要がなくなるみたいです。
ただし、グローバルステートはやたらめったら使えばいいものではないようです。
どこからでも変更できるため、「そのstateがいつ・どこで変更されるのか」「中身がどうなっているのか」を予測するのが難しくなるみたいです。
テーマカラーやログイン状態など、アプリケーション全体で本当に必要な情報に限定して使い、そうでない場合はuseStateを使うのがよいようです。
3.2 Context APIの使い方
ReactでグローバルステートをContext APIで実現するには、以下の4ステップで進めます。
-
createContextでグローバルステートを扱う箱を作る - グローバルステートにしたいデータを
stateとして定義する -
providerでアプリケーション内でグローバルステートを使える範囲を設定する -
useContextでグローバルステートを使いたい箇所から呼び出して利用する
まず、App.jsxを書き換えてcontextを作成する全体コードです:
// contextの作成に使うcreateContextをインポート
import { createContext, useState } from 'react';
import WrapperA from './WrapperA';
import ComponentB from './ComponentB';
// テキスト情報のグローバルステートを扱うcontextを作成
export const TextContext = createContext();
function App() {
const [inputText, setInputText] = useState('');
return (
// contextのproviderで囲んで、子コンポーネントからテキスト情報にアクセスできるようにする
<TextContext.Provider value={{ inputText, setInputText }}>
<div>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
{/* propsでstateを受け渡していた部分を削除 */}
<WrapperA />
<ComponentB />
</div>
</TextContext.Provider>
);
}
export default App;
createContextの引数に何も渡していないため、初期値はundefinedとなるようです。
TextContext.Providerのvalueプロパティに{ inputText, setInputText }を渡すことで、providerで囲まれた子コンポーネントすべてがinputTextにアクセスしたり、setInputTextでstateに値を入れたりできるようになるみたいです。
続いて、WrapperA.jsxを書き換えます。
propsで受け渡していた部分が不要になるので、シンプルになります:
import ComponentA from './ComponentA';
function WrapperA() {
// propsでstateを受け取っていた部分を削除
return (
<div style={{ border: '1px solid #000', marginTop: '10px' }}>
<p>WrapperA</p>
{/* propsでstateを受け渡していた部分を削除 */}
<ComponentA />
</div>
);
}
export default WrapperA;
次に、ComponentA.jsxでグローバルステートから直接データを取得します:
// グローバルステートを使うのに必要な部分をインポート
import { useContext } from 'react';
import { TextContext } from './App';
function ComponentA() {
// propsではなく、グローバルステートから直接テキストの情報を取得
const { inputText } = useContext(TextContext);
return (
<div>
<p>ComponentAで表示中: {inputText}</p>
</div>
);
}
export default ComponentA;
同じように、ComponentB.jsxも書き換えます:
// グローバルステートを使うのに必要な部分をインポート
import { useContext } from 'react';
import { TextContext } from './App';
function ComponentB() {
// propsではなく、グローバルステートから直接入力されているテキストの情報を取得
const { inputText } = useContext(TextContext);
return (
<div>
<p>ComponentBで表示中: {inputText}</p>
</div>
);
}
export default ComponentB;
Context APIを使うことで、ComponentAとComponentBの両方が直接グローバルステートからテキストの情報を利用できるようになり、中間での受け渡しやpropsでの受け渡しに関連していた部分が不要になるみたいです。
3.3 Context APIの注意点
Context APIを使う際に、異なる種類のデータを一つのcontextにまとめて管理すると、コードの理解や修正が困難になるようです。
たとえば以下のように、ユーザー情報とテーマ情報を同じcontextに入れてしまう例が悪い例です:
// 悪い例:異なる種類のデータを一つのcontextにまとめてしまっている
const AppContext = createContext();
function App() {
const [user, setUser] = useState({ name: "田 中", email: "tanaka@example.com" });
const [theme, setTheme] = useState("light");
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
<Header />
<UserProfile />
<ThemeSelector />
<MainContent />
</AppContext.Provider>
);
}
この場合、AppContextという名前からはどのようなデータが管理されているのかがわかりません。
関連するデータごとにcontextを分割するのが良い例です:
// 良い例:データの種類ごとにcontextを分割する
const UserContext = createContext();
const ThemeContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({ name: "田 中", email: "tanaka@example.com" });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function App() {
return (
<UserProvider>
<ThemeProvider>
<Header />
<UserProfile />
<ThemeSelector />
<MainContent />
</ThemeProvider>
</UserProvider>
);
}
contextを分けることで、ユーザー情報に関してはUserContext、テーマに関してはThemeContextを見ればよく、それぞれの役割が明確になるみたいです。
Context APIを使用する際は、関連するデータをまとめて管理し、異なる種類のデータは別々のcontextに分割することが重要です。
まとめ
今回はpropsのバケツリレー問題とContext APIの使い方を学びました。
コンポーネント設計を考えるうえで重要な概念だと感じたので、ここに整理しておきます。
今回の気づき
WrapperAが自分では使わないのにinputTextをpropsとして受け取っていたサンプルを見たとき、「これは確かに読みにくいな」と感じました。
Context APIを導入することで、WrapperAのコードが一気にシンプルになり、コンポーネントの役割が明確になることが実感できました。
ただし、グローバルステートは便利すぎるがゆえに乱用すると管理が難しくなるため、本当に全体で共有すべきデータにだけ使う、という判断が大事だと理解しました。
ハマりやすいポイント
-
createContextはファイルの外で定義してexportしておかないと、他のコンポーネントからimportできないので注意が必要です。 -
Providerのvalueに渡すデータを間違えると、useContextで取り出した際にundefinedになるので確認が必要です。 - 異なる種類のデータを一つの
contextにまとめてしまうと、後から管理が難しくなるみたいです。