この記事について
モダンフロントエンドにおいて、Fluxというアプリケーションアーキテクチャが存在します。
従来、Fluxの思想に従って実装を行うためには、同名ライブラリ"Flux"やReduxが採用されるケースが多かったのですが、React16.8でのReact Hooksの登場により、ライブラリに頼ることなくFluxを実現できるようになりました。
本記事では、Fluxの概念・なぜそれが必要なのかについて説明した後、React Hooksを用いたFlux実装の一例を紹介します。
使用する環境・バージョン
- OS: macOS Mojave 10.14.5
- Node.js: v12.13.0
- npm: 6.13.4
- React: 16.13.0
読者に要求する前提知識
JS, React, React Hooksが書けるだけの知識があること
Fluxとは?
Fluxとは、UIをもつwebアプリケーションを構築するときのデザインパターン/アーキテクチャです。
アプリケーションのデザインパターンといえば、他にはMVC(Model View Controller)パターンやMVVM(Model-View-ViewModel)などが存在します。
Fluxもそれらと同様に、「アプリケーションを作るときに、どういう構造にするべきなのか」という考え方の一つなのです。
Fluxが誕生した背景
MVCやMVVMがある中で、なぜFluxという思想が新しく生まれたのでしょうか。その疑問に答えるためには、背景を見ていきます。
レガシーアプリケーションの画面描画の仕組み
従来のレガシーなWebアプリケーションというのは、以下のようなスタイルでコンテンツを生成していました。
- クライアントがHTTPリクエストを送信
- サーバーサイドで、リクエストに応じたHTMLを生成→送信(必要ならばAPI・DBなどからのデータ取得を行う)
- クライアントは、サーバーから受け取ったHTMLをそのまま表示
このシステムでの特徴としては、「新しい画面・コンテンツの表示には、サーバーから新しく画面ファイルの取得→画面のリロードが必要」ということです。
しかし、これではちょっとした画面更新だけで、HTMLやCSS,JSファイルをいちいちやりとりしなければいけないので、応答速度が落ちるという欠点がありました。
SPA(Single-Page Application)の画面描画の仕組み
ここでSPA(Single-Page Application)というものが登場しました。これは、画面更新の際に
- クライアントがサーバーに画面更新に必要なデータを要求
- サーバーサイドは、画面のhtmlファイルではなく、要求されたデータのみをjson等で返す
- クライアントは、サーバーから受け取ったデータをもとに画面の一部を再レンダリング
という風にして、画面更新時のクライアントーサーバー間のやりとりを軽量にし、パフォーマンスを向上させています。
SPAの登場によって、フロントエンドの役割が大きく転換することになります。
レガシーアプリケーションでは、JSはサーバーサイドから受け取った画面のみを扱えばよかったのに対し、SPAでは画面更新時にどう正しく更新するかというところまでカバーしなくてはならないからです。
(おまけ:ちなみに、ページ生成という仕事をフロント側に任せることができるようになったため、バックエンド側の役割も変化しています。フロントエンドがデータ取得に使うためだけのAPI・マイクロサービスが多く造られるようになり、それらがBFF(Backends For Frontends)と呼ばれるようになりました)
参考:今さら聞けない!シングルページアプリケーションとは
参考:SPAにおける状態管理: 関数型のアプローチも取り入れるフロントエンド系アーキテクチャの変遷
SPAの再レンダリング時に、正しく画面を更新するために必要な考え方が「状態管理」です。
SPAの状態管理について
状態管理についてはこちらの記事が非常にわかりやすいため、これに沿って説明します。
参考:今から始めるReact入門 〜 flux編
例えば、「未読1件」の表示を画面にしているときに、新たにサーバーサイドから追加の「未読2件」の通知がきたとします。
このときフロントエンド側では、「今ある未読1件と、新たに追加された未読2件を合わせて、合計3件の未読がある」という風に判断し、表示しなければいけません。このように、「今の状態(=未読)」をずっと保持し、正しく更新し続ける機能のことを状態管理といいます。
そのときに、状態管理がされていないと、サーバーサイドからきた未読「2件」という数字をそのまま表示することになってしまいます。
Fluxで状態管理を行う流れ
Fluxはこの状態管理をReactでやりやすくしてくれます。
Fluxは以下の4つの要素で構成されています。
- View: フォームやボタンといった、アプリケーションの画面
- Action: アプリケーションの更新情報を得る為の内部API
- Dispatcher: Actionを受け取って、アプリケーションの更新作業を実際に行う関数
- Store: アプリケーションの状態の保持を行うデータストア
状態管理の流れとしては、
- Viewで(クリック等の操作を行って)どんな更新をしたいのか、Actionに通知
- ActionはViewからの通知を受け取って、dispatcherに渡す
- dispatcherは実際に状態を更新してviewに反映
画像引用元:Flux公式Doc In-Depth Overview
このように、「Storeを更新するときは、必ずDispatcherを通す」という単方向のデータフローにすることで、Storeで保持されている状態や、それに伴い発生する画面遷移の把握が容易になります。
参考:Fluxとはなんなのか
実装
ここからは、Fluxに沿った状態管理・画面更新をReact Hooksで実装していきます。
今回は、「画面に複数個のボタンがあり、それぞれ押すとon/offが切り替わる&初期状態に戻すリセットボタンも別にある」というものを作ることを想定しています。
ディレクトリ構造
Reactアプリのフォルダは、create-react-app
コマンドで簡単に作成することができます。
そのsrcディレクトリ以下で、今回関係があるところのみを抜粋して表示します。
src/
├─ App.js
├─ components
│ └─ Component.js
├─ contexts
│ └─ Context.js
├─ actions
│ └─ ActionCreater.js
└─ reducers
└─ Reducer.js
Actionの実装
Actionの実態はJSオブジェクトです。Actionは後々Dispatcherに渡されるものなので、このJSオブジェクトがDispatcher関数の引数となります。
Actionオブジェクトは、「Storeに対してどんな操作がしたいのか」というのをtype
オブジェクトに保持していることが多いです。
Actionの生成過程においては、生成actionをdispatchに渡すところまで実装することが多く、ここまでのメソッドをActionCreaterと呼びます。
以下に、ActionCreaterの例を示します。
// buttonNoで指定された番号のボタンのon/offを切り替えるためのActionを発行→dispatcherに渡す
export function toggleButton(dispatch, buttonNo) {
const action = {
type: "toggle",
data: {
button: buttonNo
}
};
dispatch(action);
}
// 全ボタンの状態を初期状態に戻すためのActionを発行→dispatcherに渡す
export function resetAllButton(dispatch) {
const action = {
type: "reset",
};
dispatch(action);
}
ここで利用している変数dispatchには、後述するdispatcher関数が格納されています。
Storeの実装
Storeの実装は、Hooksの一つであるuseReducer
の第一引数のstore
で行います。
後述するdispatcherと一緒に、Hooksの一つであるuseContext
を使ってApp内のどこでも使えるように共有する形になります。
まずはContextを作ります。
import { createContext } from 'react'
export const AppContext = createContext({
state: {
onState: Array(10).fill(false)
},
dispatch: null
});
App内のどこでも呼び出せるようにしないといけないのが「StoreとDispatcher」なので、それぞれを保持するフィールド(state
とdispatch
)を用意しています。
Contextを用意したら、今度はそれをApp全体で共有できるようにApp.js
で設定を行います。
import React, { useReducer } from 'react';
import { AppContext } from './contexts/Context';
import { AppReducer } from './reducers/Reducer';
function App() {
const initialState = {
isState : Array(10).fill(false)
};
// initialStateで、state(≒store)の初期値を設定する
// ここで作ったdispatchには、state(≒store)を操作する関数が格納されている
const [state, dispatch] = useReducer(AppReducer, initialState);
return (
<div className="App">
// こうすることで、<AppContext>以下にある(略)部分のcomponentで
// 変数stateとdispatchを呼び出せるようになる
<AppContext.Provider value={{state, dispatch}}>
(略)
</AppContext.Provider>
</div>
);
}
export default App;
Dispatcherの実装
dispatcherはStoreの変更・更新を行うものです。
これの実態はuseReducer
の第二返り値のdispatch
関数です。これの実装は第一引数AppReducer
の中で行います。
つまり、言い方を変えれば「useReducer
の第一引数で渡された関数が、第二返り値のdispatch
に格納される」のです。
Reducerは、現在のStateと新たに生成されたActionを引数として受け取り、新しいStateを返り値として返す関数です。
(nowState, action) => newState
dispatcherの実装を担うuseReducer
の第一引数「AppReducer
」を、この条件に合うよう、引数を「現在のState, Action」、返り値を「新しいState」として作ります。
export function AppReducer(state, action) {
var NewonState = state.onState.slice();
// actionのtypeによって、newStateの生成処理を変える
switch(action.type){
case 'toggle':
var i = action.data.button;
NewisPlayed[i] = !state.onState[i];
return {onState: NewonState}
case 'reset':
var filledfalse = Array(10).fill(false);
return {onState: filledfalse}
default:
return state;
}
}
ViewからのAction発行
ActionCreater, dispatcher, storeが用意できたら、いよいよViewからActionを発行してStoreを変更するロジックです。
Viewでやらなければいけないのは、以下2つです。
- ActionCreaterの呼び出し
- ActionCreaterに引数として渡すdispatcher関数の用意
例えば、「ボタンをクリックしたら、ActionCraterのtoggleButtonを呼ぶ」ためには以下のように記述します。
import React, { useContext } from 'react';
// ActionCreaterのインポート
import { toggleButton } from '../actions/actionCreaters';
function MyButton(props) {
// Contextにあるdispatcher関数を取得
const {dispatch} = useContext(AppContext);
return (
// 使いたい場所でActionCreater関数を呼び出す
<button onClick={() => toggleButton(dispatch, props.buttonno)}>
{props.buttonno}
</button>
);
}