Lesson 7
グローバルなStateの管理方法
Lesson 7
Chapter 1
グローバルなstateとは
これまでにProps、ルーティング、State、レンダリング後の処理の制御等を学習してきました。レッスン6ではレンダリングの最適化手法を学びました。本レッスンではグローバルなStateについて学習していきます。
下層のコンポーネントにおいて上層のコンポーネントで定義したStateを必要とする場合、以下のイメージのようにStateあるいはStateを更新する関数をPropsで渡します。Propsを取得した下層のコンポーネントは目的に応じてStateを参照したり、Stateを更新する関数に何らかの値を与えて実行することになります。
上層コンポーネントから下層コンポーネントへStateとその更新関数を渡すイメージ
しかし目的のコンポーネントの階層が深い場合、間にいくつものコンポーネントを経由しなければなりません。このように複数の階層でStateを共有するにはStateをPropsに渡してバケツリレー式に送らなけれならず、Stateの管理が困難になります。このようなStateはグローバルなStateにするのが有効です。
クローバルなStateとは、Propを経由せずにアプリケーション全体で使用できるようにしたStateです。グローバルなStetaはReactが提供しているcreateContextという機能で作成でき、同じくReactの提供するuseContext関数を使用して参照することができます。アプリケーション全体で使用されるデータがある場合にグローバルStateを使用すると、Propsのバケツリレーを回避できるようになるので有効です。アプリケーション全体で使用されるデータとは、例えばReactで構築したアプリケーションにサインインしているユーザー情報やアプリケーションの言語設定などです。

Lesson 7
Chapter 2
Propsのバケツリレー
バケツリレーとは
前のチャプターで登場したPropsのバケツリレーについて具体的に説明します。多層のコンポーネントによって構成されているReactアプリケーションにおいて、上層のコンポーネントからの下層のコンポーネントへ複数のコンポーネントを介してPropsを受け渡すことをPropsのバケツリレーと表現します。イメージとしては以下の画像のようになります。
バケツリレーのイメージ
このイメージは5層のコンポーネントから成るアプリケーションになりますが、途中の3層はPropsを下層へ送り出しているに過ぎません。
実際にPropsのバケツリレーをコードで確認してみます。以下の2つの図に示す名前編集アプリを例に説明します。アプリ開始時に表示される画面が上の図に示す閲覧モード、閲覧モードでパスワードを入力し送信ボタンを押した後の画面が下の図に示す管理者モードになります。アプリ画面の色のついたエリアに"田中太郎"という名前の表示と"編集"ボタンがあります。管理者モードの場合のみ、"編集"ボタンが活性化し名前を編集できるようになります。
名前編集アプリ(閲覧モード)
名前編集アプリ(管理者モード)
このアプリは以下の図のコンポーネントで構成されています。コンポーネントは上からApp→Name→EditButtonの順に階層構造になっています。
名前編集アプリのコンポーネントの設計
アプリを実装するコードは以下になります。
App.jsx
import { useState } from "react";
import { Name } from "./components/Name";
const App = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [inputedPassword, setInputedPassword] = useState("");
const password = "Admin";
const exitFromAdminMode = () => {
setIsAdmin(!isAdmin);
}
const enterAdminMode = () => {
if ( inputedPassword == password ) {
setIsAdmin(!isAdmin);
} else {
alert("パスワードが間違っています!")
}
}
const onChangeText = e => {
setInputedPassword(e.target.value);
}
return(
<div>
{isAdmin
? (<div>
<p>管理者モード</p>
<button onClick = {exitFromAdminMode} >閲覧モードに戻る</button>
</div>)
: (<div>
<p>編集するにはパスワードを入力してください</p>
<input type="text" value={inputedPassword}
onChange={e => onChangeText(e)} />
<button onClick = {enterAdminMode}>送信</button>
</div>)
}
<Name isAdmin={isAdmin}></Name>
</div>
);
};
export {App};
Name.jsx
import { useState } from "react";
import { EditButton } from "./EditButton";
const style = {
width: "300px",
height: "200px",
margin: "8px",
backgroundColor: "#e9dbd0",
display:"flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
};
const Name = ({isAdmin}) => {
const [name, setName] = useState("田中太郎");
const [isModifying, setIsModifying] = useState(false);
const onChangeText = e => {
setName(e.target.value);
}
return (
<div style={style}>
{isModifying
?(<input type="text" value={name} onChange={e => onChangeText(e)} />)
:(<p>{name}</p>)
}
<EditButton isAdmin={isAdmin}
isModifying={isModifying} setIsModifying={setIsModifying}/>
</div>
);
}
export { Name }
EditButton.jsx
const style = {
width: "100px",
padding: "6px"
};
const EditButton = ({isAdmin, isModifying, setIsModifying}) => {
return (
<button style={style} disabled={!isAdmin}
onClick={()=>{setIsModifying(!isModifying)}}>
{isModifying?"完了":"編集"}
</button>
);
}
export { EditButton };
各コンポーネントでは以下に示すStateが定義されています。
コンポーネント名 | State | 型 | 役割 |
---|---|---|---|
App | isAdmin | 真偽値 | 管理者かどうかを判定 | inputedPassword | 文字列 | ユーザーが入力したパスワード |
Name | name | 文字列 | アプリに表示される名前 | isModifying | 真偽値 | 名前が編集されたかどうかを判定 |
EditButton | - | - | - |
コードはスタイルやイベント関数が定義されていて複雑に見えますが、内容を細かく理解する必要は今回はありません。今回重要なのはState"isAdmin"の動向です。isAdminは管理者判定の真偽値を持つStateで、最上層のAppコンポーネントで定義されています。isAdminは最下層のEditButtonコンポーネントまでPropsのバケツリレーで送られます。EditButtonコンポーネントにおいて、isAdminは"編集"ボタンの活性非活性の切り替えに使用されます。このアプリのコードにおけるPropsの受け渡しのイメージは以下ようになります。
Propsの受け渡しのイメージ
中間にあるNameコンポーネントで取得したPropsから、isAdminを取り出しますが、再度Propsに代入しEditButtonコンポーネントに送り出しています。ちなみに、isAdminはNameコンポーネントにおいては使用されません。少ない階層ですが、以上がPropsのバケツリレーの実例になります。
バケツリレーのデメリット
具体例を挙げてPropsをバケツリレーするコンポーネントの設計の難しさを説明してきましたが、デメリットとしては具体的に以下のようなことが挙げられます。
- コードが複雑化する:複数のPropsを複数階層バケツリレーしてしまうと1つのコンポーネントの持つPropsが多様化し、コードが複雑化してしまいます。
- コンポーネントの再利用ができなくなる:多くのコンポーネントは再利用性を考慮し汎用的に設計されますが、バケツリレーの途中の階層になることによりコンポーネントの機能として不必要なPropsを受け取るような設計になってしまい、再利用できなくなってしまいます。
- 不要な再レンダリングが発生する:コンポーネントはPropsが更新されると再レンダリングされることを学んだと思いますが、バケツリレーの中間層のコンポーネントはバケツリレー開始コンポーネントのStateの更新によって、本来必要ないのに再レンダリングされることになります。

Lesson 7
Chapter 3
ContextでStateの管理を行う
ここまでの説明で大規模なReactアプリケーションにおいて、StateをPropsでバケツリレーすることが困難であること、Propsを使用せずにStateを共有する仕組みとしてグローバルなStateが存在することを説明してきました。本チャプターでは具体的にグローバルなStateを使用する方法を学習します。
Contextについて
ReactにはContextというグローバルなStateを管理する機能があります。ContextによりグローバルなStateを使用できるようにするには、グローバルなStateを定義する上層コンポーネントと使用する下層コンポーネントでそれぞれ以下の設定が必要になります。
グローバルなStateを定義する上層コンポーネントにおいて
- createContextでContextオブジェクトを作成する。
- グローバルなStateを使用する下層コンポーネント含むコンポーネントをContextオブジェクトのProviderで囲う。
- Providerの"value"というPropsにグローバルなStateを渡す。
グローバルなStateを使用する下層コンポーネントにおいて
- useContextを使用しグローバルなStateを受け取る。
実際に先ほど紹介した名前を編集するアプリのコードに対して、Contextを使用することで使い方を学んでいきます。
コンポーネントの準備
Propsでバケツリレーされていた管理者判定用のState"isAdmin"を今回はグローバルなStateにします。その準備として、App.jsx、Name.jsxおよびEditButton.jsxを以下のように編集しましょう。
App.jsx
import { useState } from "react";
import { Name } from "./components/Name";
const App = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [inputedPassword, setInputedPassword] = useState("");
const password = "Admin";
const exitFromAdminMode = () => {
setIsAdmin(!isAdmin);
}
const enterAdminMode = () => {
if ( inputedPassword == password ) {
setIsAdmin(!isAdmin);
} else {
alert("パスワードが間違っています!")
}
}
const onChangeText = e => {
setInputedPassword(e.target.value);
}
return(
<div>
{isAdmin
? (<div>
<p>管理者モード</p>
<button onClick = {exitFromAdminMode} >閲覧モードに戻る</button>
</div>)
: (<div>
<p>編集するにはパスワードを入力してください</p>
<input type="text" value={inputedPassword}
onChange={e => onChangeText(e)} />
<button onClick = {enterAdminMode}>送信</button>
</div>)
}
<Name />
</div>
);
};
export {App};
Name.jsx
import { useState } from "react";
import { EditButton } from "./EditButton";
const style = {
width: "300px",
height: "200px",
margin: "8px",
backgroundColor: "#e9dbd0",
display:"flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
};
const Name = () => {
const [name, setName] = useState("田中太郎");
const [isModifying, setIsModifying] = useState(false);
const onChangeText = e => {
setName(e.target.value);
}
return (
<div style={style}>
{isModifying
?(<input type="text" value={name} onChange={e => onChangeText(e)} />)
:(<p>{name}</p>)
}
<EditButton isModifying={isModifying} setIsModifying={setIsModifying}/>
</div>
);
}
export { Name }
EditButton.jsx
const style = {
width: "100px",
padding: "6px"
};
const EditButton = ({isModifying, setIsModifying}) => {
return (
<button style={style} disabled={!isAdmin}
onClick={()=>{setIsModifying(!isModifying)}}>
{isModifying?"完了":"編集"}
</button>
);
}
export { EditButton }
グローバルStateとする"isAdmin"を全てのコンポーネントのPropsから除くように記述を変更しています。
親コンポーネントでContextオブジェクトを作成する
続いてApp.jsxの中で、グローバルなStateを扱うための設定をします。以下に示すようにApp.jsxを編集しましょう。
App.jsx
import { createContext, useState } from "react"; //createContextをReactからimport
import { Name } from "./components/Name";
const AdminFlagContext = createContext({}); //createContextでContextオブジェクトを作成
const App = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [inputedPassword, setInputedPassword] = useState("");
const password = "Admin";
const exitFromAdminMode = () => {
setIsAdmin(!isAdmin);
}
const enterAdminMode = () => {
if ( inputedPassword == password ) {
setIsAdmin(!isAdmin);
} else {
alert("パスワードが間違っています!");
}
}
const onChangeText = e => {
setInputedPassword(e.target.value);
}
return(
<div>
{isAdmin
? (<div>
<p>管理者モード</p>
<button onClick = {exitFromAdminMode} >閲覧モードに戻る</button>
</div>)
: (<div>
<p>編集するにはパスワードを入力してください</p>
<input type="text" value={inputedPassword} onChange={e => onChangeText(e)} />
<button onClick = {enterAdminMode}>送信</button>
</div>)
}
<AdminFlagContext.Provider value={ isAdmin }>
<Name /> {/* EditButtonが下層に存在する<Name />を<AdminFlagContext.Provider>で囲う */}
</AdminFlagContext.Provider>
</div>
);
};
export {App, AdminFlagContext}; //AdminFlagContextをexport
追加変更箇所にはコメントを付与しました。コメントを順に追ってみます。最初に、createContextをReactからimportしています。次に、createContextを実行し、Contextオブジェクト"AdminFlagContext"を生成しています。createContextの引数には、グローバルなStateのデフォルト値を設定できますが、今回は使用しないので空のオブジェクトにしています。続いて、return文に記述された<Name />をAdminFlagContextのProviderで囲っています。Nameコンポーネントを囲う理由は、下層に存在するEditButtonコンポーネントでグローバルなStateを使用したいためです。またここで重要なのがvalueのPropsになります。valueに設定したPropsの内容がグローバルなStateとして扱えます。今回はisAdminを設定します。AdminFlagContextはEditButtonコンポーネントで使用するので最後に追加でexportしています。
子コンポーネントの中でuseContextを使用し値を受け取る
最後にEditButton.jsxの中で、グローバルなStateを使用するための設定をします。以下に示すようにEditButton.jsxを編集しましょう。
EditButton.jsx
import { useContext } from "react"; //createContextをReactからimport
import { AdminFlagContext } from "../App"; //AdminFlagContextをApp.jsxからimport
const style = {
width: "100px",
padding: "6px"
};
const EditButton = ({isModifying, setIsModifying}) => {
const isAdmin = useContext(AdminFlagContext); //ContextからisAdminを取得
return (
<button style={style} disabled={!isAdmin} onClick={()=>{setIsModifying(!isModifying)}}>
{isModifying?"完了":"編集"}
</button>
);
}
export { EditButton }
グローバルState使用のための設定箇所にコメントを付与しました。最初に、createContextをReactから、AdminFlagContextをApp.jsxからimportしています。次に、関数定義の最初にAdminFlagContextを引数にしてuseContextを実行しています。このように、Contextオブジェクトを引数にしてuseContextを実行することによって、引数にしたContextに設定したvalueの内容を取得することができます。今回はAppコンポーネントのState"isAdmin"を同名の変数に代入しています。以上で、Appコンポーネントの"isAdmin"をグローバルなStateに設定して、EditButtonコンポーネントで使用する設定は終わりです。ブラウザで動作を確認すると、"isAdmin"をPropsとしてバケツリレーしていた時と同じ挙動になるので、各自で確認してみてください。
