SPAでのデータの流れ方を設計するための一つの考え方

はじめまして。プロダクトエンジニアの[もっさん](https://twitter.com/mosmos_21)です。
この記事は [READYFORアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/readyfor) 、3日目の記事です。

## はじめに

READYFORでは今年の9月ごろに、プロジェクトを実行する実行者が、支援してくれた方々とのコミュニケーション及びリターンの管理機能などを管理するための機能を一新し、リリースしました。(詳細は[こちら](https://tech.readyfor.jp/entry/2021/09/09/150736))
今回は、そこで新しく作成した SPA での State 管理についての話をします。

## ページ構成

今回リリースした SPA の各ページでは、大きく分けてサーバーから取得したデータを表示するテーブル部分と、ユーザーが検索したい内容を入力するフォーム部分の 2 つに分かれます。検索内容は URL Parameter から渡され、 API のパラメータ及び、フォームのデフォルト入力値として利用されます。
このページが状態として持っている箇所は、大きく分けて以下の 3 つになります。

- URL のパラメータ (Query)
- 検索フォーム (Form State)
- テーブル (Resource State)

この State を何も考えずに扱うと State 間での不整合が生まれるため（formで検索した値がテーブルにだけ反映されて、URLパラメータへの反映を忘れてしまうなど）
、ひと工夫が必要です。

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661742/18ffe7e9-be43-b740-7c54-551c9b1f4b2b.png)

前提として以下のライブラリ、ツールが利用されています。

#### Next.js

site: https://nextjs.org

vercel が提供している React.js 製のフロントエンドフレームワークです。
Next.js は、本番環境に必要なすべての機能（ハイブリッド静的およびサーバーレンダリング、TypeScript サポート、スマートバンドリング、ルートプリフェッチなど）をゼロコンフィグで利用できます。

#### SWR

site: https://swr.vercel.app

Next.js の開発元である vercel が提供しているデータ取得のための React Hook ライブラリです。
SWR は、まずキャッシュからデータを返してからフェッチリクエストを送り、最後に最新のデータを持ってくるという戦略を取っています。

#### React Hook Form

site: https://react-hook-form.com

高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリです。
最小限のコードでフォームを作成でき、 TypeScript との親和性が非常に高いため、型安全なフォームを作成できます。

#### openapi-typescript

site: https://github.com/drwpow/openapi-typescript

openapi の定義から TypeScript の型を生成するための node ライブラリです。

#### pathpida

site: https://github.com/aspida/pathpida

Next.js の Page コンポーネントの定義から自動的に URL の型定義ファイルを生成するライブラリです。

## 型の依存関係

![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661742/f56de91e-8a44-5195-9626-d4ed3484a5e9.png)


基本となる型定義は openapi-typescript を用いて、schema.yaml から自動的に型定義ファイルを生成しています。生成された型をもとに、API Query の型、Form で扱うための型及び、ページの Query Parameter の型を定義します。基本的には生成された型からエイリアスを貼り、必要な型を抽出、結合して、利用します。ただし、Form においては input の型の都合上、例外的に独自の型を宣言する場合がありますが、その場合でも型の宣言・利用は Form State と Form View 内で完結します。Form Hook が Props として受け取る型は加工せず、また、他のパートへの型の変更の影響を与えることはありません。

## データフロー

データフローの考え方において、以下の図のようなレイヤに分けて考えます。各モジュール、親コンポーネントのそれぞれのレイヤで、どのような責務を持つかを定義します。

![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661742/57df6928-3919-51dd-7156-00c55c5a2998.png)

### Query

このレイヤーでは、 Next.js の useRouter を利用して URL パラメータにアクセスします。
Query では以下の責務を持ちます。

- ページに必要なパラメータの型定義(pathpida を利用)
- パラメータの型の変換と型の保証
- 必須パラメータのチェック(存在しない場合はエラーへの遷移)
- Template レイヤへの値の受け渡し

### Template

このレイヤでは Query レイヤからのデータを受け取り Hook へのデータの受け流しを行います。また Hook と View の繋ぎ込みを行います。
表示の出し分け等は行いますが、基本的にロジックを持ちません。

### Form State

ユーザの操作により、変化する値を管理するレイヤーです。検索フォームの入力や、ページ情報、ソート情報の保持、ハンドラ定義などもこのレイヤでおこないます。
実装上は Hook として切り出していて、 State 管理には React Hook Form を利用しています。UI の制限により、State で保持する値や型を変更している場合には、この State 内に閉じた型を宣言します。データの変化がある場合は Next.js の push アクションを経由して URL の変更し、 Query レイヤーの State の変更します。

### Resource State

受け取った Props を元に API からリソースの取得をおこないます。Form State と同じく実装上は Hook として切り出していて、State の管理及びリソースの取得には SWR を利用しています。 ユーザの操作によって検索条件が変更になった場合、Form の State から直接パラメータを受け取ることはせず、必ず Query と Template を経由してパラメータを受け取ります。

## まとめ

今回のまとめた複数　State 管理の考え方は、 Flux のイメージから設計されています。複数モジュール間におけるデータの流れを一方向にし、ユーザの操作による検索パラメータの適用を push アクションに集約しています。これにより、複数の State の整合性が保ちやすくなりました。

明日は[Takepepeさん](https://twitter.com/takepepe)によるフロントエンドのテストに関する話です！