Lesson 7

グローバルなStateの管理方法

Lesson 7 Chapter 1
グローバルなstateとは

これまでにProps、ルーティング、State、レンダリング後の処理の制御等を学習してきました。レッスン6ではレンダリングの最適化手法を学びました。本レッスンではグローバルなStateについて学習していきます。

下層のコンポーネントにおいて上層のコンポーネントで定義したStateを必要とする場合、以下のイメージのようにStateあるいはStateを更新する関数をPropsで渡します。Propsを取得した下層のコンポーネントは目的に応じてStateを参照したり、Stateを更新する関数に何らかの値を与えて実行することになります。

上層から下層コンポーネントにPropsを渡すイメージ.png 上層コンポーネントから下層コンポーネントへ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のバケツリレーと表現します。イメージとしては以下の画像のようになります。

2023-02-28-17-42-38.png バケツリレーのイメージ

このイメージは5層のコンポーネントから成るアプリケーションになりますが、途中の3層はPropsを下層へ送り出しているに過ぎません。

実際にPropsのバケツリレーをコードで確認してみます。以下の2つの図に示す名前編集アプリを例に説明します。アプリ開始時に表示される画面が上の図に示す閲覧モード、閲覧モードでパスワードを入力し送信ボタンを押した後の画面が下の図に示す管理者モードになります。アプリ画面の色のついたエリアに"田中太郎"という名前の表示と"編集"ボタンがあります。管理者モードの場合のみ、"編集"ボタンが活性化し名前を編集できるようになります。

2023-03-01-08-43-44.png 名前編集アプリ(閲覧モード)

2023-03-01-08-46-38.png 名前編集アプリ(管理者モード)

このアプリは以下の図のコンポーネントで構成されています。コンポーネントは上からApp→Name→EditButtonの順に階層構造になっています。

名前編集アプリの設計.png 名前編集アプリのコンポーネントの設計

アプリを実装するコードは以下になります。

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の受け渡しのイメージは以下ようになります。

2023-03-01-09-44-28.png 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を定義する上層コンポーネントにおいて

  1. createContextでContextオブジェクトを作成する。
  2. グローバルなStateを使用する下層コンポーネント含むコンポーネントをContextオブジェクトのProviderで囲う。
  3. Providerの"value"というPropsにグローバルなStateを渡す。

グローバルなStateを使用する下層コンポーネントにおいて

  1. 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としてバケツリレーしていた時と同じ挙動になるので、各自で確認してみてください。