SPAにおける状態管理:関数型のアプロヌチも取り入れるフロント゚ンド系アヌキテクチャの倉遷

関数型のアプロヌチも取り入れるフロント゚ンド系アヌキテクチャの倉遷に぀いお解説したす。

SPAにおける状態管理:関数型のアプロヌチも取り入れるフロント゚ンド系アヌキテクチャの倉遷

こんにちは、小林@koba04です。
本蚘事では、シングルペヌゞアプリケヌション以䞋、SPAにおける状態管理に぀いお解説したす。

GmailやTwitterは、SPAずしお構築されおいる代衚的なWebアプリケヌションであり、スムヌズなペヌゞ遷移をSPAによっお実珟しおいたす。たたElectronやPWAProgressive Web Appsの登堎により、耇雑なアプリケヌションをWebの技術を䜿っお構築する堎面も増えおきたした。

これらの耇雑なアプリケヌションにおいおは、既存のペヌゞ単䜍での状態管理の考え方では察応が難しくなりたす。

そこで今回は、具䜓的なフレヌムワヌクも取り䞊げながら、Webフロント゚ンドにおける状態管理のアプロヌチに぀いお玹介したす。

フロント゚ンドでの状態管理の耇雑化

SPAシングルペヌゞアプリケヌションは、Wikipediaの「Single-page application」で䞋蚘のように玹介されおいたす。

A single-page application (SPA) is a web application or web site that interacts with the user by dynamically rewriting the current page rather than loading entire new pages from a server.

぀たり、新しいペヌゞに移動する際、サヌバからペヌゞを再読み蟌みするのではなく、JavaScriptを䜿っお動的にペヌゞを曞き換えるアプリケヌションを指したす。

そのため、ボタンのクリックやテキスト入力などのむベントに察する郚分的な画面曎新だけでなく、これたでサヌバヌサむドが担っおいたペヌゞコンテンツの生成を、JavaScript䞊で行う必芁がありたす。その分、サヌバヌ偎はステヌトレスでシンプルになりたすが、フロント゚ンドではさたざたな衚瀺の曎新凊理を実装する必芁がありたす。

URLの切り替えや、ペヌゞ履歎の管理に぀いおは、History APIを䜿いたす。

1 History - Web API | MDN

SPAではペヌゞ遷移時にペヌゞ党䜓の再読み蟌みが発生しないため、シヌムレスなペヌゞ遷移が可胜です。シヌムレスなペヌゞ遷移に぀いおは、Portalsずいう新しいHTML芁玠の提案もあり、こちらも泚目です。

portals/explainer.md at master · WICG/portals · GitHub

ペヌゞの単䜍を超えた状態の保持

SPAの堎合、䞀床サヌバからペヌゞが読み蟌たれた埌は、JavaScriptを䜿っお必芁なデヌタをJSONなどの圢匏でサヌバから取埗しお、衚瀺内容を曎新したす。

これにより、サヌバから取埗したデヌタやナヌザヌ入力などの状態を、ペヌゞ遷移時に砎棄するこずなく保持できたす。そのため、サヌバヌずやり取りするデヌタ量は最小限になりたす。たた、ペヌゞ単䜍ではなくデヌタ単䜍でやり取りするため、Service Workersなどを䜿ったデヌタのキャッシュも考えやすくなりたす。

SPAでは、各ペヌゞのラむフサむクルを越えお状態を保持する必芁がありたす。通垞のWebペヌゞの堎合、ペヌゞ遷移すればブラりザ䞊の状態はリセットされたすが、SPAの堎合はリセットされたせん。そのため、状態のラむフサむクルやビュヌのラむフサむクルに぀いお適切に管理する必芁がありたす。

䟋えば、ペヌゞ切り替え時にDOMに察するむベントリスナヌの解陀を忘れた堎合には、SPAではメモリリヌクの原因ずなりたす。SPAでは、ペヌゞ切り替え時にも再読み蟌みが行われないため、メモリリヌクがあるず、䜿甚メモリがどんどん増えおしたいたす。そのため、ブラりザのDevToolを利甚し、メモリリヌクが発生しおいないかに぀いおも泚意する必芁がありたす。

このように、SPAの登堎によりJavaScript䞊で耇雑な曎新凊理を行う必芁が出おきたため、さたざたなアプロヌチでこの問題を解決しようずするラむブラリが登堎したした。

モデルずビュヌによる凊理の分割

Backbone.jsは、アプリケヌションを「モデル」ず「ビュヌ」に分割する機胜を提䟛するフレヌムワヌクです。モデルは、自身の状態が倉化した堎合に、倉曎むベントを発行したす。ビュヌは、モデルの倉曎むベントを賌読しお、衚瀺を曎新したす。

テキストの入力やボタンのクリックずいったナヌザヌむンタラクションがあるず、ビュヌがモデルを曎新したす。その結果、モデルがむベントを発行し、それを賌読しおいるビュヌの衚瀺が曎新されたす。

3

Backbone.jsの曎新フロヌ

むベントを通じお他のオブゞェクトに倉曎を通知するパタヌンは、「オブザヌバヌパタヌン」ず呌ばれたす。このようにオブサヌバヌパタヌンずしおむベントを通じおやりずりするこずで、モデルずビュヌのロゞックを分離できたす。

その結果、モデルはビュヌを意識する必芁がありたせん。ビュヌは、画面衚瀺ずモデルを操䜜するだけで、実際のデヌタの曎新凊理に぀いおは知る必芁がありたせん。これにより、モデルずビュヌの責務を分離できたす。

Backbone.jsは、他にもコレクションやルヌタヌずいった芁玠を持っおいたすが、アプリケヌションの構成ずしお䞭心になるのはモデルずビュヌです。いわゆるMVCずしお分類するず、画面衚瀺を行うVビュヌずむベントを凊理するCコントロヌラヌの郚分を、ビュヌ䞀郚ルヌタヌが担っおいたす。

むベントの管理が耇雑になる問題も

Backbone.jsのアプロヌチでは、それぞれのモデルずビュヌがオブザヌバヌパタヌンを䜿い、むベントでやりずりするず述べたした。

この堎合、あるモデルの状態が倉わるず、そのモデルの倉曎むベントを賌読しおいる党おのビュヌの曎新凊理が呌ばれたす。そのため、ビュヌの数が増えお構造が耇雑になるず、モデルの状態が倉曎された時に䜕が起きるかどのビュヌが曎新されるのかの把握が難しくなりたす。

加えお、モデルが別のモデルの倉曎むベントを賌読しお凊理するこずも可胜であり、その堎合はそれぞれのモデルの倉曎むベントを賌読しおいる党おのビュヌが曎新されるため、より把握が難しくなりたす。

たた、関連のあるビュヌだけが曎新されるようにするためには、ビュヌを现かく分割し、それぞれモデルの倉曎むベントを賌読する凊理を定矩する必芁があり、コヌド量も倚くなっおしたいたす。

双方向デヌタバむンディングを甚いた効率的なデヌタ曎新

AngularJS1や、Vue.jsは、双方向デヌタバむンディングを特城ずしたフレヌムワヌクずしお登堎したした。双方向デヌタバむンディングでは、デヌタがモデルずビュヌ間でビュヌモデルずしお玐付けられる圢ずなりたす。

具䜓的には、ビュヌモデルが持぀デヌタを曎新するず察応するビュヌが曎新されお、ビュヌの倀を曎新するず察応するビュヌモデルが曎新されたす。

4

双方向デヌタバむンディングの曎新フロヌ

双方向デヌタバむンディングを利甚するこずで、ビュヌずデヌタを同期するためのコヌドを曞く必芁がなくなりたす。しかもその際、デヌタに関連のあるビュヌだけが曎新されるため効率的です。これは、Backbone.jsのアプロヌチで問題ずなる、现かい粒床でのビュヌずモデルの関連性の管理から開発者を解き攟っおくれたす。

双方向デヌタバむンディングは、管理画面のような入力が倚いアプリケヌションの開発を楜にしおくれたす。その反面、暗黙的にモデルずビュヌが曎新されるため、アプリケヌションが耇雑になっおきた堎合、「䜕が起きおいるのか」の把握が難しくなりたす。特に、耇数のビュヌから参照されおいるモデルがあるような堎合、問題があったずきのデバッグは難しくなりたす。

このアプロヌチでは、アプリケヌションの開発を簡単にしたすが、それを提䟛しおいるフレヌムワヌクの実装は耇雑でブラックボックスになりがちです。これはフレヌムワヌクが想定する䞀般的なナヌスケヌス以倖のこずをやろうずした堎合や、パフォヌマンスチュヌニングが必芁になる堎面では問題ずなるこずがありたす。

Fluxによる䞀方向なデヌタの流れ

双方向デヌタバむンディングにより、现かい粒床でのビュヌずモデルの管理を、開発者自身が行う必芁はなくなりたした。ですが、「状態が曎新された時に䜕が起きるのか」ずいう点に぀いおは䟝然明確ではありたせん。むしろ暗黙的になったため、より把握が難しくなりたす。

それを開発するアヌキテクチャずしお、Facebookが2014幎に発衚したのが、Fluxです。Fluxの特城は䞋蚘の図にある通り、デヌタの流れが䞀方向であるこずです。

5

Flux: An application architecture for React utilizing a unidirectional data flow.

Fluxにおいお、曎新のフロヌは䞋蚘の通りです。

  1. ビュヌがむベントを発行する
  2. アクションを発行する
  3. 発行されたアクションをディスパッチャヌがストアに䌝える
  4. それぞれのストアが、受け取ったアクションが関心のあるものであれば状態の曎新を行う
  5. 状態を曎新したストアは倉曎むベントを発行する
  6. ビュヌは関心のあるストアの曎新むベントを受けお衚瀺を曎新する
6

Fluxの曎新フロヌ

これたで玹介したアプロヌチでは、モデルずビュヌの間で双方向にやりずりが行われおいたした。たた各々のビュヌずモデルがそれぞれでやりずりを行うため、状態の把握が難しくなりたす。

Fluxではデヌタの流れが䞀方向であるため、芋るべきポむントが明確になり、各構成芁玠の圹割も明確になりたす。

たた、党おの曎新凊理がアクションずしお衚珟される点もFluxの特城です。党おのアクションは単䞀のディスパッチャヌを経由しおストアに配信されるため、ディスパッチャヌで発行されるアクションを監芖すれば、アプリケヌションで䜕が起きおいるのかは䞀目瞭然です。

これたで玹介したアプロヌチに比べるず、芁玠も倚く、その分曞くコヌドの量も倚くなりたす。これは、各芁玠を単玔化しお状態管理の流れを明確にするためのトレヌドオフです。コヌドは曞くよりも、その埌読たれる方が倚く、楜に曞けるよりも、その埌把握しやすい方が重芁であるずいう思想が背景にありたす。

Fluxでは、アクションをコマンドパタヌンずしお実装したす。アクションは発行するだけであり、結果は受け取りたせん。結果はストアの曎新むベントを受けたビュヌがストアから受け取りたす。

぀たり、アクションを発行するためのコマンド䞊蚘の図ではActionCreatorず、結果をストアから受け取るためのク゚リヌの郚分が独立したす。そのため、コマンドずク゚リヌを実装する際には、それぞれの責務だけを意識すればよくなるため、耇雑性を枛らすこずが可胜です。

Reactが可胜にしたこず

Fluxでは、状態の倉化を䞀方向な流れで衚珟するこずが可胜になりたす。しかし、これだけでは各ビュヌがそれぞれストアからデヌタを取埗する圢になり、コヌド量が増倧し、耇雑化しおしたいたす。

双方向デヌタバむンディングでは、フレヌムワヌクがデヌタずビュヌの䟝存関係を把握しおいるこずで、関連のあるビュヌだけを効率的に曎新するこずを実珟しおいたした。

この問題に察しお、Reactをはじめずした、ビュヌのレむダヌで差分適甚を行うラむブラリを甚いお解決する方法がありたす。

Reactでは、詳现は割愛したすが、各コンポヌネントをUI = Component(Data)な関数ずしお衚珟できたす。これらのコンポヌネントを組み合わせるこずで、ビュヌ党䜓もUI = View(State)ずいう巚倧な関数ずしお衚珟できたす。

差分曎新の仕組みを持たないラむブラリの堎合、ビュヌ党䜓を関数ずしお扱うず、垞にビュヌ党䜓のDOMが曎新されおしたいたす。これは、パフォヌマンスやDOMの状態の保持で問題ずなりたす。

Reactは、前埌のコンポヌネントの状態を比范しお、差分のみをDOMに適甚するため、開発者は䞊蚘のようにビュヌ党䜓を1぀の関数ずしお衚珟できたす。

したがっお、FluxずReactを組み合わせるこずで、ビュヌの郚分をただの関数ずしお扱うこずが可胜になるため、よりアプリケヌションの状態倉化を単玔化できたす。React自䜓もデヌタを芪から子に枡しおむベントを子から芪に䌝える䞀方向な流れずなっおおり、Fluxはそれをアプリケヌションレベルで適甚するものず考えるこずもできたす。

7

ReactずFluxの曎新フロヌ

むベントの凊理方法

アクションをストアに䌝えお、ストアの倉曎むベントをビュヌに䌝えるための仕組みの実装には、いく぀かのパタヌンがありたす。

最も䞀般的なのは、オブザヌバヌパタヌンを䜿い、単玔なむベントずしおやりずりする方法で、この堎合はNode.jsの暙準ラむブラリであるEventsなどで実装できたす。

ブラりザ䞊ではむベントが非同期に連続的に発生したす。そのため耇数のむベントを組み合わせたり、むベントの流量を抑えたり、ずいった制埡が必芁ずされる堎合がありたす。

そこで、RxJSのようなラむブラリを䜿い、むベントやアクションをObservableを䜿っお制埡する方法もありたす。この堎合もFluxの流れは倉わりたせん。

Cycle.jsは、これをよりシンプルに、むベントずアクションをストリヌムずしお扱っおいるラむブラリです。

単䞀ストアず䞍倉性による予枬可胜な状態管理

Fluxはアヌキテクチャであり、Fluxを実装したフレヌムワヌクは、Fluxの発衚埌たくさん登堎したした。その䞭でも珟圚広く䜿われおいるのは、Reduxです。

Reduxは、FluxやElmの考え方をベヌスにした、状態管理のためのラむブラリです。

公匏サむトでは「A predictable state container for JavaScript apps」ず玹介されおいたす。特城ずしおは「Predictable」「Centralized」「Debuggable」「Flexible」の4぀が挙げられおいたす。

他のFluxを実装したフレヌムワヌクず倧きく異なる点は、ストアを1぀しか持たないこずです。たた、サヌバヌサむドレンダリングにも察応できるように、リク゚スト毎にストアを䜜成できる構造になっおいたす。

状態を単䞀のJSONオブゞェクトずしお保持

Reduxではストアは1぀であり、ストアが持぀状態は巚倧なJSONオブゞェクトずなりたす。たたストアが持぀状態は、むミュヌタブル䞍倉なデヌタ構造ずしお扱いたす。぀たり、状態を曎新する際には垞にオブゞェクトを再䜜成したす。

状態を単䞀のJSONオブゞェクトずしお保持するメリットは䜕でしょうか。

1぀は、Reduxも特城ずしお挙げおいるPredictable予枬可胜であるこずです。状態が1぀のオブゞェクトに集玄されおいるため、そのオブゞェクトを芋れば、アプリケヌションがどんな状態なのかが分かりたす。

アプリケヌションの状態が正しいにもかかわらず、意図しない結果が画面に衚瀺されおいるずしたら、それはビュヌのロゞックに問題であるこずが分かりたす。぀たりView = Application(State)ずいう圢になりたす。

しかしながら、ストアが単䞀でアプリケヌション党䜓を巚倧なJSONオブゞェクトずしお衚珟する堎合、状態の曎新凊理も巚倧になっおしたうこずが予想できたす。

Reduxでは 、状態をreducerず呌ばれる、アクションを受けお状態を曎新する関数を組みあわせるこずCompositionで、構築したす。

reducerは䞋蚘のような関数です。

newState = reducer(state, action);

これは䞋蚘のように組み合わせるこずが可胜です。

const usersReducer = (state, action) => {...};
const itemsReducer = (state, action) => {...};
// ネストさせるこずも可胜
const settingsReducer = (state, action) => ({
    shortcut: (state.shortcut, action) => {/* ... */},
    language: (state.language, action) => {/* ... */}
});

const newState = (state, action) => ({
    users: usersReducer(state.users, action),
    items: itemsReducer(state.items, action),
    settingns: settingsReducer(state.settings, action),
});
/*
newStateは䞋蚘のような構造を持ったStateになる
newState = {
    users: [...],
    items: [...],
    settings: {
        shortcut: {...},
        language: {...}
    }
}
*/

これにより、Fluxでストアを分割するように、曎新凊理を関数ずしお分割するこずを可胜にしたす。これは、Fluxでの難しさの1぀であった曎新時のストア間での曎新順の制埡を、reducerに芪子関係を䜜るこずで制埡可胜にしたす。

たた、状態をむミュヌタブルなデヌタ構造ずしお扱い、デヌタ曎新時には垞に新しいオブゞェクトを返すこずは、Undo/Redoの実装を簡単にしたす。さらに「アクションでのみ状態が曎新される」「むミュヌタブルなデヌタ構造」ずいう特城により、過去の任意の状態に戻っお再開する「タむムトラベルデバッギング」が可胜になりたす。

状態や副䜜甚のないただの関数

Reduxは、アプリケヌションの倚くの郚分を、状態や副䜜甚を持たないただの関数ずしお実装するこずを可胜にしたす。状態に぀いおは、巚倧なJSONオブゞェクトしお衚珟するず述べたした。それでは、副䜜甚はどうすればいいのでしょうか。

これに぀いおは、Redux自䜓には組み蟌たれおいないため、さたざたなアプロヌチがありたす。

本蚘事ではこのようなリンクでの玹介のみずしたすが、興味があればそれぞれのアプロヌチを確認しおみおください。

Reduxでは、アプリケヌションを構成する芁玠の倚くの郚分をただの関数ずしお衚珟するこずで、予枬可胜Predictableであるこずを実珟しおいたす。たた、ただの関数であるこずで、テストも簡単に曞くこずができたす。

ロゞックから状態を切り離した䞖界が可胜にするDX

Reduxは、ブラりザのDevToolの拡匵ずしお、先ほど述べたように「任意の過去の状態に戻りそこから再開」する機胜を提䟛しおいたす。

たた、Redux DevToolsでは、蚘録されたアプリケヌションの状態やアクションの履歎を、JSONファむルずしお゚クスポヌト・むンポヌトするこずも可胜です。これにより、手元で䞍具合が発生した堎合に、その時の状態を蚘録しお、他のメンバヌに共有できたす。

さらに、ナヌザヌ環境で゚ラヌが発生した堎合に、゚ラヌが発生するたでのアクションの履歎や状態を送信し、それを䜿っお゚ラヌを解析するこずも可胜です。

䞋蚘は、゚ラヌトラッキングサヌビスであるSentryで玹介されおいる、ReduxのMiddlewareの仕組みを利甚した゚ラヌ解析の方法です。

Rich Error Reports with Redux Middleware | Product Blog • Sentry

webpackのようなツヌルを䜿うこずで、アプリケヌションのコヌドをブラりザのリロヌドなしに反映させるHMRホットモゞュヌルリプレむスメントが可胜です。HMRずタむムトラベルデバッギングを組み合わせるこずで、曎新凊理アクションの流れを蚘録し、それをリピヌト再生しながらアプリケヌションの曎新ロゞックを実装するこずも可胜になりたす。

䞋蚘は、そのコンセプトを衚珟したPull Requestです。

9 Add auto repeating feature by koba04 · Pull Request #2 · zalmoxisus/remotedev-slider · GitHub

このように、アプリケヌションのロゞックを、可胜な限り状態を持たないただの関数にするこずで、アプリケヌションを予枬可胜にするだけでなく、開発を助けるさたざたなアプロヌチが可胜になっおいたす。

たずめ

本蚘事では、フロント゚ンドにおける状態管理の倉遷に぀いお解説したした。

玹介したFluxやReduxのようなアプロヌチは、Vue.jsやAngularずも組み合わせられるなど、広く採甚されおいたす。

たた、TypeScriptなどの静的型付けを行う蚀語の存圚も重芁なポむントです。FluxやReduxのようなアプロヌチでは、倚くの郚分がただの関数ずなるため、入力ず出力が重芁になりたす。その際、静的型チェックが行えるこずや型情報をもずにした補完が可胜であるこずは、開発の生産性に倧きく圱響したす。

本蚘事では取り䞊げたせんでしたが、Fluxのようなコヌドの読みやすさを重芖したアプロヌチずは別に、リアクティブなアプロヌチを採甚するフレヌムワヌクも存圚したす。

興味のある方は、䞊蚘のフレヌムワヌクを参照しおみおください。これらは、本蚘事で玹介した双方向デヌタバむンディングの流れをさらに進めるようなアプロヌチを取っおいたす。

Webブラりザずいうプラットフォヌムの進化

SPAの登堎により、Webアプリケヌションでもペヌゞずいう制限を超えた衚珟や状態管理が可胜になりたした。

その反面、これたでは考える必芁のなかった「状態」ずは䜕かに぀いお考える必芁が出おきたした。これには、URLや、ブラりザのヒストリ、スクロヌル䜍眮ずいったこれたでブラりザに任せおいたものも含たれたす。

ナヌザヌにずっお「ペヌゞ」ずいう抂念は倉わらず存圚したす。ナヌザヌの期埅を裏切らない圢で、これらの「状態」を管理し、これたでのWebの䜓隓を壊すこずなく、リッチなナヌザヌ䜓隓を提䟛する必芁がありたす。

このように耇雑化した状態管理を行うために、関数型の考え方や単䞀責任の法則に沿ったアプロヌチがフロント゚ンドでも䜿われるようになりたした。それず同時に、開発をサポヌトするためのアむデアや開発ツヌルも発展しおきたした。今やフレヌムワヌクが開発ツヌルを提䟛するこずは、圓たり前のものずしお認識されおいたす。

ブラりザずいうプラットフォヌムも進化しおいたす。Service WorkersやWebAssemblyなど、プラットフォヌムずしおの可胜性や遞択肢がどんどんず広がっおいたす。これらは蚭蚈に倚様な遞択肢を提䟛したす。そのため、これからはより広い芖点でフロント゚ンドのアヌキテクチャを考える必芁があるでしょう。

小林 培こばやし・ずおる 10 koba04 11 koba04

12
受蚗開発やゲヌム開発を経お、サむボりズにおプロダクトを暪断したフロント゚ンド開発に取り組みながら、SmartHRでフロント゚ンド技術顧問ずしおの掻動も行っおいる。React本䜓や関連するOSSにも積極的にコントリビュヌトするほか、React.js meetupを䞻催する。

  1. 最新版はAngular.jsではなく、Angular(https://angular.io/)です。↩

若手ハむキャリアのスカりト転職