半年前の自分に教えたいApp Router周りのこと
Concurrent周りを理解するところから始まるよねー
「Reactはとにかくユーザーにいち早く画面をお届けしたい」っていうのを押さえたい
ここからかな?
- SSR: 先にHTMLを渡しちゃうことでいち早く画面を表示
- Streaming SSR: ↑ページ単位でやってたら時間がかかるので、Suspense境界ごと(コンポーネントごと)に分割してSSR
っていう布石がまずあった
これね。Streaming SSR。
App RouterでgetServerSidePropsがなくなったのは、この考え方とマッチする
↑ページ単位でやってたら時間がかかるので、Suspense境界ごと(コンポーネントごと)に分割してSSR
Suspenseを上手く使えばいろんな指標が改善するよねー、っていう理解が必要
Nested Layoutも、「どうせ遷移またいで見せているコンポーネントなら、そのまま見せておくほうがはやいよね」っていう感覚で理解できそう。
どこまでLayoutにするかは開発者が決められるが、やりたいことはこの「ページ単位ではなくてコンポーネント単位の読み込み」と理解しておけばOK(さっきの動画のスクショ)
じゃあSuspenseてどう使うねん?っていう話
実は公式ドキュメントには「整備にもうちょい時間かかります」とある
Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
もともとはReact.lazyとセットの機能だったけど、現状はデータフェッチングで主に使われている。「Suspenseの内部でPromiseがthrowされたらSuspendし、解決されたらもとに戻る」みたいな感じ。uhyoさんのBookがすごいわかりやすい。
React業界、uhyoさんに支えられている部分が大きい
Tanstack QueryやSWRとかも(不完全ながらも)Suspenseに対応している。でもApp Routerでおそらく一番使うことになるのがAsync Server Componentsとの組み合わせ
const AsyncServerComponent = async () => {
const data = await fetch(...)
return (...)
}
みたいな感じでコンポーネント内部でデータ取得ができるし、AsyncServerComponent
はデータ取得時にSuspendするのでSuspenseで包んであげる。
<Suspense fallback={<Skeleton />}>
<AsyncServerComponent />
<Suspense />
最寄りのSuspense境界までSuspendすることになっているので、何も包まなければ最悪ページ全体がSuspendする。ページ全体がSuspendしたときのfallbackとして、App Routerではloading.tsxが各ページごとに書けるようになっている。
つまりApp Routerでは密かにページ全体がSuspenseで囲まれていることになる
「でもあんまりページ全体でSuspendさせたくないよねー、表示できるところはいち早くお届けしたいよねー」っていう前提があり、データ取得責務は自然と子に降りていくことになる。
getServerSidePropsではいろんなAPIをまとめて叩いてページコンポーネントのpropsに詰めて渡していた。
今は、Suspenseを使ってデータフェッチを分割し、Concurrent(並列)に処理する。そうすることではやくなる。この感覚が大事。
となると、コンポーネントの責務も取得するデータごとに決まってくる。/api/todos
を叩くのは<TodoList />
であってほしい、みたいな感覚。
APIエンドポイントは基本的に「オブジェクト(モデル)」ごとに作られている。
-
/api/todos
:Todoオブジェクトのリスト -
/api/todos?id=xxx
:Todoオブジェクトの単体
したがって、オブジェクト単位でコンポーネントを設計することになる。OOUI的な考え方と接続する。
これ以前にも、modelベースのコンポーネント単位はよく言われていたけどね
「データ取得がモデルベースで行われる」という前提のもと、データ取得の責務を切り分けたいのでContainer/Presenterパターンをよく使う。
ひとつのモデルコンポーネントに対して
- データ取得のためのContainerコンポーネント
- モデルに対しての見た目を表現するViewコンポーネント
- Containerでデータ取得してSuspendするときのfallbackに渡すLoadingコンポーネント
がセットになるイメージ。
他のも色々混じってるけど、TodoTableの例。これはTodo[]
に対するコンポーネント。
TodoTableの要旨はこれで
使用するページ側はこんな感じになる
↑はデータ取得にTanstackを使っているのでContainerからClient Componentだけれど、
- データ取得はServerConponentで
- 複雑なインタラクションがあるViewはClientComponentで
ってなるのが頻出パターン
小さなContainerに着目した資料
このコンポーネントはSuspenseで囲うべき!っていうのが名前でわかるように、SuspendableなコンポーネントはXXXContainerで統一する、ってう副次的な効果もある(賛否両論?)
コンポーネント設計がモデル単位に収束していくんだから、ディレクトリ構成もモデルごとに凝集していくといいよねー、っていうような話をここに書いた
App RouterではRoute Segment(ページ単位のまとまり?をそう呼んでいる)ごとのファイル凝集(このページでしか使わない関数はそのページとCo-locationしておく、みたいなこと)が可能になっていてとても便利なんだけど、
Route Segmentが肥大化するのもそれはそれでつらいので、
- modelに関心があるやつはとりあえずmodel/にいれとく
- modelに関心がなくて、複数箇所で使うようなやつはcommon/にいれとく
- modelに関心がなくて、ページ特有のものはRoute Segmentにいれとく
みたいな判断基準がいいかなと思っている。
(NITS) ルーティングに現れないフォルダ分けは、 _model/
みたいにアンダーバー始まり(Private Folders)にするのが良いと思います
↑これはapp/内に入れるならってことですよね?補足ありがとうございます!
ここまでで重要なメンタルモデルは拾えている気がするけど、さらにServer Conponentの理解を深めたければここらへんがおすすめ
また、Suspense込みのRoutingの挙動をちゃんと把握しようと思うとtransitionを理解する必要がある
Suspense-enabled routers are expected to wrap the navigation updates into transitions by default.
Next.jsのこまかい機能は使えばわかるから大丈夫!
あと重要なのはキャッシュか。ISRとかは死語になりました。
なんでキャッシュに注目が当たり始めたのかはここらへん読むと感覚がつかめるかも?さすがに嘘?
Note that there is still a network request per each keystroke. What’s being deferred here is displaying results (until they’re ready), not the network requests themselves. Even if the user continues typing, responses for each keystroke get cached, so pressing Backspace is instant and doesn’t fetch again.
なんかもうちょっとある気がするけど一旦満足した