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を使います。

1History - 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が可能にしたこと

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く