React Hooks指向のfrontendのarchitectureを考える (2022)
(↓これは2022年の時に思っていた話で、2023ではRSCも登場したし自分の中の方向性も色々変わっています

)
最近(2022年)のReactの思想として、「個々のComponentを賢くする」という流れがある
親のComponentが全てfetchして、子にデータを伝搬していくのではなく
個々のComponentが各々でデータをfetchして表示する
headerと、footerで同じデータをfetchして使用する際に、2回requestが送られずに再利用してくれる
Reduxだとこれは少し難しい
request回数を減らすためには、親でuseEffectを呼ぶ必要があり、そうするとCustom Hooksの中で処理を完結できない
何かしらの運用でカバーが必要になる
Layerを作るなら以下のようになる
この図はUsecaseの立ち位置がちょっと雑

dirauthenticatons/
types.ts // 型
(entities.ts) // Entity
(usecases.ts) // UseCase
repositories.ts // Repository
useLogin.ts // Hooks
useLogout.ts // Hooks
..
components/ // View
業務やfeatureに依っては、 usecases
や entities
は不要なこともある
例えば、fetchしてきたものを整形して表示するだけのfeatureでは不要になる
各Layerの責務
View
見た目に関することのみが責務
Componentとstyleを書く
Component内ではhooksか親からもらったJSONを色付けてして表示するだけ
ロジックはここには書かない
tsconst Component = () => {
// ここにはロジックを書かない。hooksを呼ぶだけ
return <div>...</div>
}
もっと厳格にやるならContainerとPresentationに分けるべきなんだろうな

Custom Hooks層
責務ごとに分けられたCustom Hooksを書く
ロジックをここに凝集させる
例えば、以下のようなことはcustom hooksの中でやる
actionを起こしたときのhandlerとなる関数の定義
react-hook-formのuseFormを呼ぶ
react-queryのuseQueryを呼ぶ
ReduxのuseSelectorを呼ぶ
etc.
react-queryを使っている場合は、fetcher関数がusecaseやrepositoryに相当する
tsexport const usePosts = () => {
const { data } = useQuery(
queryKeys.posts(),
Repository.getPosts, // これがUsecase.getPostsだったりする
{..}
);
return {..};
};
返り値はViewに渡されるわけだが、必要最低限のものを提供する
この返り値は、hooksの内部で使っているlibraryに依存させない
libraryをwrapする
こうしておけば、状態管理libraryがReduxからreact-queryに変わったときも、修正箇所を最小限に抑えられる
流石に、Reduxとreact-queryは思想が違いすぎるので本当にそこだけで抑えられるかは微妙だけど、Viewで直接useSelectorを書いている時に比べればかなり減らせるはず

Error Handlingはここで行う
UseCase層
不要なことも多いが、ロジックの多い箇所ではこの層を用意する
例えば、POST系の処理など
classは使う必要がない。関数の列挙で十分
内部でRepositoryの関数を呼ぶ
hooksに依存しないロジックはここに書けばいい
抽象度の高い処理の列挙になる事が多い
失敗する処理はthrowする
Hooks層がhandlingしてくれる
内部でRepositoryのErrorのhandlingも行うこともある
例えば、login処理の中で、付随してmodal表示などをする場合に、
後者が失敗したとして、loginそのものを失敗させる必要はない
loginが失敗したときだけ、失敗しました、と表示すれば良い
Repository層
backendとの通信や、WebStorageへの保存などの具体的なロジック
返り値はEntityの型になる
ここで、外部から受け取ったデータを完全に整形してUsecaseやHooksに返す
backendはRESTかもしれないし、GraphQLかもしれない
その差分はRepositoryで閉じ込める
RESTからGraphQLに変わった時に、Repositoryの修正だけで済むのが理想
Repositoryの内部では大まかに3パーツに分けられる
request部分
GETしたりPOSTしたりする処理
constructor部分
外部リソースをEntityに整形するための関数
外部リソースが返す型の定義
外部リソースの返す型
内部のEntityと全く同じであっても、定義しておいたほうが良い
通信でErrorが生じた場合などは基本的にthrowする
4xx、5xx等の場合はthrowさせる
react-queryやSWRは、fetcherがthrowしたかどうかで失敗を判定している
Eitherなどで表現した場合、retryとかisErrorとかの動作が正しく機能しない
LocalStorageやCookieの操作もRepositoryに書く
UsecaseやHooksから見れば、それらはインターネットの向こうにあるのか、ローカルにあるのかは意識しないで良い
Entity層
domain logicがあれば、それを実装する
Typeを定義するだけのfile
Entityの型などを定義する
この型定義がプロダクトの根幹である
ここがどうしようもないとfeature全体がどうしようもなくなる

どこでError Handlingするか?
適宜変えればよいが、基本方針としては以下のようになる
Repositoryは失敗したらthrowさせる
UseCaseは失敗したらthrowさせる
Hooksがhandlingする
↑これはreact-queryを使用していることを前提としているので、この限りではない

要は、Repository内でこういう風には書かないということ
外部から配列を受け取る例
repositoriies.tsexport const getPosts = async () => {
try {
const result = await get<ResPost[]>(`/posts`);
return result.map(toPost);
} catch {
return []; // こういう風には書かない
}
}
通信に失敗した場合に空配列を返している
そうではなく、Hooks層までerrorを伝搬させる
RepositoryやUsecaseの中でErrorを揉み消さないほうが良い
明確な意図があるならそうしても良いけど
失敗時にretryなどをしてくれる
上記のものからもっと改善できる
RSCを導入することで、層を薄くできそう
react-queryなどがそもそも不要
PBFをもっと強める
こうじゃなくて
こうする
実際はこんな感じになる気がした
つまり、Hooksに階層を入れて2種類に分ける

例えばフォームのvalidationなどが書かれたもの
まあ、これについてはComponentと同じfileに書けば良いじゃん、という気もする
が、fileがでかくなるのももにょる
でもfile名のprefixとかでルール付けするよりはよぽど良いな
特定のViewに依存しないロジック
こういう風に2つに分けることで、
一番大きな円(View+Hooks, Repository)の中で、
だから、2つ目以降の円(Hooks, UseCase, Entity)内では仕様を満たした型を使用できる
form validationは?
Viewの責務
上の話は、Custom Hooksのレイヤーが雑になっている
Custom Hooksの中で、別のCustom Hooksを呼ぶことはあるわけで、
ここに依存関係の曖昧さが存在する
安定度の高いhooksもあれば、頻繁に修正されるhooksも存在するはずで、
それを何らかのルールに則ってレイヤー化した方が本当は良いはず

はいまのところ問題にぶつかってないので、そこまでモチベがないけども、複数人があれば問題は生じるはず
Componentの中にでは、Atomic Design等のlayerが存在する
userIdを取得するhooksみたいなのが、いろんなところのcustomHooksで使われすぎるようにならないか?
1つのものに大量に依存があるのは大丈夫なのかどうか
>つまりここで言う「便利な Hooks」は「特定のバックエンドに密結合にする代わりに、めちゃくちゃ便利にバックエンドを使えるようになる Hooks」
まさに
ESPってなんだ

react-queryを使った場合の、repository的なものの置き場所の話
案1 そもそもbackendが整形したデータを返せばいい
GraphQLを使っているのと同じ感じ
それができれば苦労しない

案2 queryFnの中でやる

はこれでやってた
デメリットはrefetchのたびにこの変換が行われること
例えば、react-queryからreudxに移行した場合も変更が少なくなると思う

案3 customhooksの、値をreturnする箇所で変換する
なんかぐちゃぐちゃになりそうな気もするけど?
ここでrepository的なものを呼べばそうでもないか
useMemoで適宜囲う
「データの整形」とは別に、custom hookの仕事として「データの計算」がある場合に、一緒にやった結果をmemo化できるので、案2よりも効率化される可能性がある
案4 useQueryの select
を使う
docs全部読まないといけないなと思いつつ実践しているが、やはり量が多すぎて抜け落ちてしまう
dataが未定義になることをきにしないでいい
select内に含める関数はuseCallbackで作った方が良い
あるいはhooks外に作ればいいか
これめっちゃ良いな
でかいresponseを返すRESTがあった場合に、
{a: Big1, b: Big2}
hooksの責務としては、 a
と b
は別々に管理したかった
だからkeyを分けて
['a']
のrepositoryで↑をまるごと取って、 a
だけ返す
['b']
のrepositoryで↑をまるごと取って、 b
だけ返す
ということをしていたので、requestが部分的に倍になっていたが、
↑わかりにくすぎるので、ちゃんと書こう

このアプローチを使うことで、それを軽減できる
でもこれだと、server側のキモい型がhooks内まで出てきちゃうな
hooksをrepository内に作ればどうにかなる
まあ、そもそもGraphQLを使えばこの辺は問題にならないか
ということで、案2のままで行く