Open60

次世代 React CSR SPA 開発 学習メモ

ピン留めされたアイテム
MasuqaTMasuqaT

React 18 で Concurrent Rendering が実現される。これを使う実装パターンやライブラリが整った状態を当面の次世代と位置づけ、そこに向けて現在の React SPA 開発からの差分をキャッチアップしていく学習のメモをここに残す。

(SSR や ISR はメモ作成者の興味対象外なので特に触れる予定はない)

MasuqaTMasuqaT

このスクラップが固まる前に追加されたような Web 標準機能は取り入れる。

MasuqaTMasuqaT

従来の React CSR SPA 開発でも React.lazy によって Suspense の機能は使えた。遅延読み込みさせることでコード分割が可能になり、読み込み速度[1]と記述のしやすさが向上した。

https://ja.reactjs.org/docs/code-splitting.html#reactlazy

脚注
  1. 正しくは必要部分の読み込み時間もしくは見かけ上の時間が短くなるということで、往復回数が増える分すべてを読み込むまでの時間は伸びる。 ↩︎

MasuqaTMasuqaT

Suspense を、ボイラープレート的に使ったりそれをコードレビューで確認するような、普段の実装の 8 割程度を占める難度の内容を理解するので十分であれば、サマリ的なこの文書を読めばよい。

https://qiita.com/uhyo/items/bbc22022fe846fd2b763

差分

  • 読み込み処理は isLoading のような状態でなく Suspense で宣言的に表現できる
  • 複数コンポーネントをまとめてサスペンドできるため、複雑な表示制御は手続き的処理より簡単に書ける
MasuqaTMasuqaT

Suspense の処理を学ぶにはこのハンズオンが良い。実装パターンやライブラリの使い方を軽く読むのに十分なレベルの知識が得られるはずだ。

https://zenn.dev/uhyo/books/react-concurrent-handson

差分

  • ローレベルでは、コンポーネント内処理で Promise を throw して Suspense で受け取るという流れになる
  • Suspense という概念の導入によりコンポーネントのステートに注意事項が増える
  • Suspense は(CSR では)主にサスペンドの境界を定める
  • render-as-you-fetch パターンが(ある程度実現しやすい形になって)登場する
MasuqaTMasuqaT

render-as-you-fetch パターンは、手前味噌な概念だが「犠牲的モジュラフロントエンド」 https://zenn.dev/occar421/scraps/a02ac7b40c0df1 の実現に大きな役割を持つと考えている(fetch の物理処理と論理処理の間をいい感じにやるライブラリはさらに必要)。

MasuqaTMasuqaT

Suspense を使いこなすところに到達するにはこのハンズオンが良い。Suspense によりより良い UX を提供するための知識が得られるはずだ。

https://zenn.dev/uhyo/books/react-concurrent-handson-2

差分

  • トランジションという優先度の低いステート更新という概念ができる
  • トランジションは「表の世界」でなくまず「裏の世界」(平行世界)に反映され、終わり次第「表の世界」に現れる
  • 実際にはトランジションは記録され、「表の世界」が変わるたびに毎回「裏の世界」(平行世界)が生成される
  • useTransition から得られる startTransitionisPending により、トランジションさせるための関数とその処理中のフラグのペアが得られる
MasuqaTMasuqaT

どこかに載っていたのか、それを見て自分で思いついたのかは不明なこと。Suspense の概念の捉え方について。

  • 「Suspense=読み込み処理」や「Suspense=(変数としての)読み込み中フラグが不要」という捉え方ではかなり不十分
    • Suspense 自体は処理を待つ(サスペンド)という結構抽象的な概念
      • 今まではフラグなどの変数と手続き的な処理で実現していたがこれが変わる
      • 何の理由で待っているかは Suspense 部分は知る由もない
      • その一番わかりやすい具体例として、読み込み処理が終わるまで待つ、という例が頻繁に紹介される
    • すべてのコンポーネントはサスペンドする可能性がある
      • 今までは読み込み処理が終わるまで待つことをフラグで表現すると局所的だったがこれが変わる
      • どこが読み込むか(局所性を)考えるのは不要になるが、逆に、常にどこでも考えるべき観点になった
  • ErrorBoundary と似たようなもの
    • こちらも(最終ゲートとしてでなく)Suspense のような使い方で積極的に使っていく可能性がある
  • データ取得コードと取得後の表示コードの近接の効果は無視できない
    • 親で data を渡してくるのも動くには動くが一度に読むべきコード量が増えてしまう
MasuqaTMasuqaT

末端までのどこでデータ取得処理があるかは気にする必要がなくなる。
これは、設計上、データ取得ライブラリの特性と相乗効果がある。

MasuqaTMasuqaT

render-as-you-fetch を実現しやすくなる(と筆者が予測している)GraphQL の利点などはこの記事でおさらいできる。

https://zenn.dev/yoshii0110/articles/2233e32d276551

https://speakerdeck.com/quramy/graphqltofalsexiang-kihe-ifang-2022nian-ban

MasuqaTMasuqaT

RESTful API だったり Redux だったりを使い、よく考えていればそれらでも render-as-you-fetch は可能ではあるだろう。

React Location のようなルーターを使う手もあるかもしれない。

MasuqaTMasuqaT

GraphQL の Fragment Colocation とその自動巻き上げ(希望的には Location に応じた root での必要最小限集合の fetch)が利用できるのは大きいと思う。

MasuqaTMasuqaT

render-as-you-fetch はここが整理されていると思う。

https://scrapbox.io/tosuke/Render-as-You-Fetch

  • fetch-on-render, fetch-then-render, render-as-you-fetch の違い
MasuqaTMasuqaT

「コンポーネント A はロケーション B の時にデータ C を要求する」ことが事前に解析できれば、コンポーネントが render される前に読み込みが走り render が走りサスペンドし、随時表示されていくのでうれしいはず。

Fragment Colocation で推奨されるような GraphQL と React とでそれぞれ import する鎖を活用すれば良さそうではある。ただし、useFragment のようなものは Suspense 実現のために必要になる。


export const PetPageGql = gql`${PetQuery}`;

export const PetPage() {
  const [initialQueryRef] = useSomething();
  const [petReference] = useQueryLoader(PetPageGql, initialQueryRef);
  return <Pet reference={petReference} />
}
export const PetQuery = gql`query Pet {
  pet() {
    name
    ...PetDetail
  }
  ${PetDetailFragment}
}`;

export function Pet({ reference }) {
  const { data: { pet: { name } } } = usePreloadedQuery(PetQuery, reference);
  return <div><span>{data.name}</span><PetDetail reference={reference} /></div>
}
export const PetDetailFragment = gql`fragment PetDetail on Pet { description }`;

export function PetDetail({ reference }) {
  const { data: { description } } = useFragment(PetDetailFragment, reference);
  return <p>{description}</p>
}
MasuqaTMasuqaT

render-as-you-fetch

イメージ

<Suspense fallback={<div>foo-bar</div>}>
  <Component data={data} />
</Suspense>
MasuqaTMasuqaT

これをやると GraphQL をクライアントで使う感じがつかめそう。Fragment Colocation も。

https://github.com/Quramy/gql-study-workshop

Fragment Colocation はメモの筆者のイメージと違い、機能やライブラリではなくファイル構造設計に近いような話だった。大きく構えすぎていた。

MasuqaTMasuqaT

サブコンポーネントでの useFragment 相当の取得方法ができて初めて Fragment Colocation が本領を発揮しそうだし、render-as-you-fetch も理想に近づきそう。

MasuqaTMasuqaT

GraphQL で出てくる概念諸々

"GraphQL Federation"

現状は Apollo での実装(Apollo Federation)しかない?

https://moneyforward.com/engineers_blog/2021/12/20/graphql-federation-2/

メモ作成者の現在の状況からすると必要ではないので、自分では深追いはしないでおく。

Persisted Query

https://qiita.com/Quramy/items/b3943a0c27f3ade2c57d

https://speakerdeck.com/indigolain/persisted-querywoyatutemita

感じたメリット

  • 意図しないクエリを封じる
  • リクエストのペイロード量の削減

感じたデメリット

  • 登録クエリ管理が難しそう
  • API リクエスト時のオンデマンドなクエリ変化という利点が失われてしまうのではないか?

Apollo の Automatic Persisted Queries は面白そうだった。一応、意図しないクエリで負荷がかかる課題は Query depth, Query complexity, Query rate limit の制限である程度緩和できるはず(リンクは後述)。

MasuqaTMasuqaT

とりあえずはライブラリやサーバーが良しなにやってくれると信じて、フロントエンドではこれらは検討しなくていいんじゃないかな…最適化段階か何かでちょっと弄れば大丈夫そうだし。

MasuqaTMasuqaT

(TODO)
production ready react 18 + graphql

https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/

https://docs.github.com/ja/graphql/overview/resource-limitations#rate-liresource-limitationsmit

他にも GraphQL の資料はあるはず

海外の有料の資料(Production Ready GraphQL)

エラーのリソース定義のブログ記事(社の Slack にリンクがあるので探せばリンクは見つかる)

React 18 の Production Readiness はどう考えればよいのだろうか。

MasuqaTMasuqaT

(TODO)
一応はバックエンドの GraphQL サーバー事情も少々…

MasuqaTMasuqaT

何故かわからないが、GraphQL のクライアントライブラリとして、触る前から Apollo はなんか違うという気がしたので後回しにする。Meta 謹製の Relay か urql を触ってみる。

React Suspense や Fragment Colocation を使ったときに便利なものを見つけ出したい。

https://nulab.com/ja/blog/nulab/graphql-apollo-relay-urql/

(TODO)

  • relay
  • gqless 🤔
  • urql
MasuqaTMasuqaT

そもそもの Data-Fetching ライブラリを使う利点。

※ 下記記事にまとめた

https://zenn.dev/occar421/articles/why-data-fetching-library-for-spa

ライブラリの書いてある推しポイントでない、設計上の優位性が確かにあると思うのだが。あまり触れている記事が無い。GraphQL クライアントには Data-Fetching ライブラリの機能を(特に区別せず)統合しているものもあるため、それも調べてみる。


Suspense の登場によって Organisms 単位でデータ取得しても読み込み状態の表示がちぐはぐになるわけでなく、(それを使う)Pages 単位で好きなまとまりに対していい感じに見せることができるようになる。以前はちぐはぐになるか Ornganisms - Pages 間で密にデータをやり取りしないといけなかっただろう[1]

脚注
  1. 上に読み込み状態を好きに制御させるのが大変。こういうことが起きる中間管理職的なコンポーネントとその設計は今までしてなかったので実際はわからないが…。 ↩︎

Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
MasuqaTMasuqaT

API エンドポイント間の依存性の表明の問題の解決を OpenAPI 等+コードで書くのでなく GraphQL Scheme に書くことに求める前提なら、設計上の利点を求めるときに思想と一番合うのは Relay ということになるだろう。

MasuqaTMasuqaT

Recoil は全くわからないが、API エンドポイント間の依存を解決するのは Recoil のデータフローグラフと相性がいいかもしれないし、(recoil-relay がある GraphQL は置いておいて)REST API でも(コードとしてあらわすことになるが)Recoil を使うといい感じに解決してくれるかも。

Mutation で atom を弄ると effect で関係する Query の atom に働きかけてその atom が再読み込みされるとか(Recoil エアプ)

MasuqaTMasuqaT

(TODO)
ルーティングライブラリ

React Router v6.4 とか React Location とか。

render-as-you-fetch を実現する仕組み、もしくは、パラレル読み込みを実現する仕組み。Page の loader を export してルーティングコンポーネントに何を読み込むかを事前に表明することで実現する。Fragment Colocation を想起させるのはないこともない。

MasuqaTMasuqaT

Concurrent の文脈で、React か何かがすべて読み込み先を自動収集してくれるといいんだけども。もしくは、単一 useQueries を export しておくとか。

MasuqaTMasuqaT

Recoil 等のデータフローグラフ

※ 下記へ切り出した

https://zenn.dev/occar421/scraps/e35d9a9a590b3b

MasuqaTMasuqaT

Recoil で React Query っぽいのを作るとこんな感じなんだろうか。(一度も動かしてないので多分動かない。)

MasuqaTMasuqaT

順番は分からないが、Server State 専用だったデータ取得ライブラリを抽象化して Global State に全体適用したのが Recoil だろうか。

MasuqaTMasuqaT

値のセットされていない atom の値を参照すると Suspend する。

MasuqaTMasuqaT

react-query 風になるよう乱暴なコードを書いてみた。(キャッシュキーは配列でなく文字列)

https://github.com/occar421/my-etudes/blob/main/recoil-primitive-sandbox/src/recoil-query.ts#L3-L30

使い方

https://github.com/occar421/my-etudes/blob/main/recoil-primitive-sandbox/src/RecoilQueryProto.tsx#L5-L68

こんな感じで包むと、気軽に Server State を扱える気になるが、Global State とのつながりが失われてしまい、わざわざ Recoil で裏を作る意味がなくなってしまう。Atom や Selector は export するようなインターフェースにしたい。(API は react-query と全く違うものになるため、recoil-query という名前ではない方が良いだろう。)

MasuqaTMasuqaT

atom effect により、State 利用者は技術的詳細を知らなくて済む。infrastracture 的コードの部分で勝手に裏を入れ替えることが可能。

単なる JavaScript メモリー上の Client State を、LocalStorage に永続化するようにする場合、atom の定義場所で atom effect を書けば事済む。

きれいな関心の分離。データノード定義と利用ソースの分離。

MasuqaTMasuqaT

CSS Cascading Layers そのものと React での活用

MasuqaTMasuqaT

おそらく下のように分ければいいのではないだろうか

  1. reset: リセット
  2. design-system.base: デザインシステムの基礎スタイル
  3. design-system.component: デザインシステムのコンポーネント
  4. base: (有れば)アプリケーション固有の基礎スタイル
  5. piece: アプリケーション固有の小規模コンポーネント
  6. area: Atomic Design での Organisms レベルコンポーネント
  7. page: ページコンポーネント
  8. app: アプリケーション
MasuqaTMasuqaT

単純に、reset 層以外はコンポーネント階層やディレクトリ設計に従えば良さそうなので、そちらを先に考えることにする。
https://zenn.dev/link/comments/247e7563b09145

CSS Cascading Layers が入ってもそんなに書き味や設計は変わらないだろうというのもある。

MasuqaTMasuqaT

単なる CSS (or CSS Modules)なら今すぐに使える[1]し、CSS in JS ならばライブラリが対応し次第使えるだろう。

脚注
  1. ユーザーが古いバージョンのブラウザを使っている可能性は少しあるのだが。 ↩︎

MasuqaTMasuqaT

Tailwind みたいな utility の層をどこに置くかという問題はあった。

そもそも使うのかどうか、使うべき属性は何か、という議論はあるが…

MasuqaTMasuqaT

(TODO)
Container Query とか :has() セレクタによるコンポーネント CSS の書き方の変化

MasuqaTMasuqaT

(TODO)
コンポーネント階層・分類やディレクトリ設計

MasuqaTMasuqaT

拙作の考えるコンポーネント階層を晒すと以下の通り[1]

  • Piece
  • Area
  • Page
  • App

Piece は Piece から使われることはある。あくまで階層であって、Design System や Local などの種類や、user や pet などのドメインの括りは別軸にあたる。また、これはあくまでコンポーネントだけであって、コンポーネント以外の要素は登場していない。

脚注
  1. papa になるよう単語を合わせている。 ↩︎