Flex Plugins の Reactサンプルを読み解く


はじめに

みなさん、こんにちは。
KDDIウェブコミュニケーションズの Twilioエバンジェリストの高橋です。

今回は、Flexプラグインを作成するときに事前に理解をしておきたい FlexプラグインでのReactコンポーネント構造について、CLIで作成されるサンプルコードを読み解きながら解説していきます。

FlexプラグインとReactの関係

Flexでは、UIにReactを採用しています。よって、Reactベースでコンポーネントを追加することで、FlexのUIを自由に変更することができます。そのため、Flexでプラグインを開発しようとすると、Reactの知識が不可欠になります。
ここがプラグイン開発のハードルになっていることも事実で、ちょっとでもハードルを下げるために、今回はあまりReactを知らない方でも、なんとなくこんな感じなのねっていうところがわかってもらえればと思って記事を書くことにしました。
とはいえ、80%以上が僕自身の理解を深めるためのものです。

ハンズオン

なにはともあれ、まずは手を動かしながら学習を始めましょう。

Flexプラグイン CLIの準備

以下の記事を参考に、Flexプラグイン CLIをインストールしてください。
Twilio Flex Plugins CLI

そして、記事にも書かれているように、plugin-cli-testという名前のプラグインを作成しておいてください。

ファイルの構成

出来上がったプラグインは以下のようになっています。

.
├── README.md
├── build
│   ├── plugin-cli-test.js
│   └── plugin-cli-test.js.map
├── jest.config.js
├── node_modules [1102 entries exceeds filelimit, not opening dir]
├── package-lock.json
├── package.json
├── public
│   ├── appConfig.example.js
│   └── appConfig.js
├── src
│   ├── CliTestPlugin.js
│   ├── components
│   │   ├── CustomTaskList
│   │   │   ├── CustomTaskList.Container.js
│   │   │   ├── CustomTaskList.Styles.js
│   │   │   └── CustomTaskList.jsx
│   │   └── __tests__
│   │       └── CustomTaskListComponent.spec.jsx
│   ├── index.js
│   └── states
│       ├── CustomTaskListState.js
│       └── index.js
├── webpack.config.js
└── webpack.dev.js

今回学習する必要があるファイルは、以下の6つです。

  • src/CliTestPlugin.js
  • src/states/index.js
  • src/states/CustomTaskListState.js
  • src/components/CustomTaskList.container.js
  • src/components/CustomTaskList.Styles.js
  • src/components/CustomTaskList.jsx

src/CliTestPlugin.js

プラグインの開始ポイントとなるのがこのファイルです。

具体的には、22行目のinit(flex, manager)関数が開始時に呼ばれることになりますので、ここから順を追って見ていきます。

23行目にあるregisterReducers(manager)は、同じファイルの37行目から定義されています。
ここで重要な技術要素が、React-Reduxです。

<参考> React-Reduxって?

Reactもよくわからない人に、React-Reduxを説明するのは難しいのですが、ここでは非常に簡単に説明します。
Reactというのは、コンポーネントという単位でプログラムを作成していくことで、プログラムを部品化し、再利用性を向上させることができます。たとえば以下の図をみるとわかるように、Flex上には色々なコンポーネントがあり、それぞれを部品化することで管理がしやすくなっています。

Reactコンポーネントは通常親子関係をもっており、例えば上の図でいうと、2のMainContainerの中に、3、4、5、16、17のコンポーネントがあります。さらに、16の中には、6と9のコンポーネントがあり、6には更に7と8のコンポーネントが入っています。
Reactでは、各コンポーネントに処理を分割して管理するため、そのコンポーネントで表示するデータなどの状態もコンポーネント単位で管理されます。そのため、親コンポーネントが子コンポーネントを作成するときにデータを渡したり、データの代わりに関数を渡すことで、子コンポーネントから親コンポーネントにデータを渡す仕組みになっています。
このような仕様により、コンポーネントにはそのコンポーネント内の処理だけでなく、他のコンポーネントとのデータのやりとりも実装しなくてはいけません。

この作業を軽減することができるのが、React-Reduxです。
具体的には、コンポーネント内で状態を管理するのではなく、状態を管理する専用のストアと呼ばれる領域を利用し、各コンポーネント間でのやり取りにもこのストアを利用します。ストアに対するデータのやり取りを行う仕組みをReducerと呼び、ストア内のデータはすべてReducer経由で行います。
先程のソースコードの44行目にあるmanager.store.addReducer(namespace, reducers)で、このプラグインで利用するReducerをFlexプロジェクトのストアに登録しています。

つぎに、ソースコードの25〜30行目を見てみます。
ここでは、flex.AgentDesktopView.Panel1.Content.add(...)をしていることがわかります。これは先程のコンポーネント全体図をみるとわかるように、16番のAgentDesktopView.Panel1コンポーネント内に新しいコンテンツを追加することを意味します。
以下の図のように左側の上の方に黒いバーが表示されているのが、今回のプラグインで生成したコンポーネントです。

では次に、add関数の中に指定されている<CustomTaskListContainer .../>について見ていきましょう。
この実態は、ソースコードの5行目でインポートされているcomponents/CustomTaskList/CustomTaskList.Containerです。

src/components/CustomTaskList.Container.js

このファイルは、先ほど説明したReact-Reduxの仕組みを使って、実際のプラグインコンポーネントを呼び出す中間ファイルです。

具体的には、15行目で指定しているように、React-Reduxに用意されているconnectという関数を使って、コンポーネント定義ファイル(同じディレクトリ内にあるCustomTaskList.js)を呼び出すのですが、その際に、mapStateToPropsmapDispatchToPropsを引数に渡します。最初の引数であるmapStateToPropsは、コンポーネントに変数を渡すためのものです。具体的には、isOpenという変数をコンポーネントに渡しています。
とりあえず、変数の内容については後回しにして、実際のコンポーネント定義ファイルを見ていきましょう。

src/components/CustomTaskList.jsx

こちらが今回のプラグインで表示するコンポーネントを定義したものです。

7行目で、パラメータとして受け取ったisOpenを判定し、falseだった場合はそのまま何もせずに終了します。
isOpentrueだった場合は、This is a dismissible demo componentという文字列と、closeという文字列を表示しています。後者については、クリックが可能になっており、クリックしたときのイベントに、props.dismissBarを指定しています。
文字列のスタイルについては、<CustomTaskListComponentStyles>というファイル(CustomTaskList.Style)で定義しています。

src/components/CustomTaskList.Styles.js

こちらはコンポーネントのスタイルを設定しているものです。

少なくともここまでの情報で、プラグインコンポーネントが表示される仕組みの概要は理解できたかと思います。
ではいよいよ積み残してきたコンポーネントに引き渡す変数や、コンポーネントで実行する関数ハンドラの仕組みについて見ていきましょう。

src/states/index.js

src/states内のファイル(index.jsとCustomTaskListState.js)がReducerを定義したものです。index.jsがインデックスの役割になっています。
一番最初に説明したCliTestPlugin.jsの6行目には以下のような行があります。

src/CliTestPlugin.js
import reducers, { namespace } from './states';

これにより、まずsrc/states/index.jsが参照され、namespacereducersがインポートされます。
reducersについては、9〜11行目でreduxのcombineReducersを使ってreducersが返却されています。

src/states/index.js
export default combineReducers({
  customTaskList: CustomTaskListReducer
});

combineReducersは複数のReducerをマージする役目があります(ただし、このコンポーネントではCustomTaskListStateの1つしか定義されていません)。
整理すると、このコンポーネントでは、CustomTaskListStateというReducerが定義されていて、ストアに対する値のやり取りはそこで指定することがわかります。

src/states/CustomTaskListState.js

正直、このファイルの仕組みが一番難解です。
まず、このファイルのreduce関数(11行目〜)がReducerとなりstateを返します。stateの初期値は、3〜5行目で指定されている通り、isOpen: trueです。

ここで重要な概念が、Actionsです。Reducerを使ってストアの値を操作するには、Actionsというオブジェクトを利用する必要があります。Actionsの構造は以下のようになっています。

Actionsオブジェクト
{
    type: "アクションの種類を一意に識別できる文字列またはシンボル",
    payload: "アクションの実行に必要な任意のデータ",
}

これはsrc/states/CustomTaskListState.js内でいうと、以下の部分になります。

src/states/CustomTaskListState.js(7〜9行目)
export class Actions {
  static dismissBar = () => ({ type: ACTION_DISMISS_BAR });
}

これは、dismissBarという名前のActionsを定義し、このアクションのTypeはACTION_DISMISS_BARであり、payloadは省略されていると読むことができます。

で、実際にこのReducer経由で値を取得するときに実行されるのが、以下の部分です。

src/states/CustomTaskListState.js(11〜23行目)
export function reduce(state = initialState, action) {
  switch (action.type) {
    case ACTION_DISMISS_BAR: {
      return {
        ...state,
        isOpen: false,
      };
    }

    default:
      return state;
  }
}

ここでのポイントは、stateの返し方です。具体的には、

return {
  ...state,
  isOpen: false,
}

のように、既存の値を残しながら差分をマージすることです。これによって、他の値を上書き削除しないようにします。

では最後に、コンポーネントの呼び出し時(connect実行時)の動作を解説します。
具体的には、src/components/CustomTaskList.Container.jsの以下の部分です。

CustomTaskList.Container.js
const mapStateToProps = (state) => ({
    isOpen: state['cli-test'].customTaskList.isOpen,
});

const mapDispatchToProps = (dispatch) => ({
  dismissBar: bindActionCreators(Actions.dismissBar, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(CustomTaskList);

先ほど説明したように、このファイルは中間ファイルで、CustomTaskListコンポーネントを呼び出すときのパラメータを指定しているのが、mapStateToPropsです。
state['cli-test'].customTaskListでストア内に格納されている現在のstateを参照することができます。コンポーネントを作成するときのisOpenパラメータは初期値であるtrueになっています。よって、CustomTaskListコンポーネントには、{ isOpen: true "}というパラメータが渡されることになります。
ちなみに、state['cli-test']cli-testは、stateのネームスペースです(src/states/index.js内で指定してあります)。

次に、コンポーネントからストアの値を更新するのが、mapDispatchToPropsです。今回はdismissBarという名前のハンドラとしてコンポーネントには渡しているので、コンポーネント内では、onClick={props.dismissBar}という形で利用することができます。
実際の動作としては、reduxに用意されているbindActionCreatorsを使ってReducerアクションのバインドを行っています。先程解説したとおり、dismissBarアクションは、TypeとしてACTION_DISMISS_BARを返しますので、結果的にdismissBarを呼び出すと、isOpenfalseが設定され、stateを利用しているコンポーネントの表示が消えることになります。

まとめ

多分一度読んだくらいでは理解できないかと思いますが、実際にコードを色々と弄ってみることで理解が深まると思いますので、ぜひチャレンジしてみてください。


Twilio(トゥイリオ)とは

https://cloudapi.kddi-web.com
Twilio は音声通話、メッセージング(SMS /チャット)、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウド API サービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。

自己紹介  
高橋克己(Katsumi Takahashi) 自称「赤い芸人
グローバル・インターネット・ジャパン株式会社 代表取締役
株式会社KDDIウェブコミュニケーションズ Twilio事業部エバンジェリスト

2001年より大手通信事業者の法人サービスの教育に携わり、企業における電話のしくみや重要性を研究。2016年よりTwilio事業部にジョインし、Twilioを使ったスマートコミュニケーションの普及活動を精力的に行っている。
2015 Hall of Doers
2019 Twilio Champions