【key-front】Next.js App Routerの素振り(ディレクトリ構成・CSS選定・Compositionパターン)
モチベーション
- 毎週木曜日Slackのkey_frontチャンネルでハドル機能を使いお題に対してメンバー同士ディスカッションをする時間を15〜30分程度設けている
- 今回は個人開発を通して学んだNext.js App Routerについて取り上げる
- ファシリテーターは筆者なので、事前に読み込んで気になった点などをスクラップに投げていく
- 開催日は11/24(金)で最終的に議事録として結論をまとめる
過去に開催した関連してそうな内容
TODO
前回の補足内容について触れる。もし素振りの段階で別の課題や共有したい内容があれば別途スクラップを作る。
- Next.js App Routerのディレクトリどうするか問題
- Next.js App RouterでランタイムCSS-in-JSがNGな理由
- compositionとSC・CCのInterleaving
- Next.js App Routerでpre-rendering周りはどうなっているか
- RSC devtoolsの所感
Next.js App Routerのディレクトリどうするか問題
- ルーティングが期待値になっていることは確認済み
前提
- Next.js12からNext.js13(App Router)へ移行する
- リポジトリルートにsrcディレクトリ(既存アプリのソース)がある
- Page Routerみたいにルーティングさせたい
appディレクトリ以下に移行済みのソースを配置する
前提を踏まえappディレクトリ以下にもcomponentsやその他ディレクトリを用意し移行済みのファイルを配置すると丸いのでは?と思ったので添付画像の構成にした。
少なくともappディレクトリ以下ならuse client
などApp Router独自の宣言などが書かれていても不思議ではないし、appディレクトリ以下でだけSC・CCの関係性を考えれば良くなり余計な心配をする必要がなくなる。
次の記事が勉強になりました。
Private Folders
appディレクトリ以下でPrivate Foldersは使用していない。理由としては(pages)ディレクトリ
以下がルーティングの責務であることが明確なため。
Route Groups
appディレクトリ以下はpage.tsxであればルーティング判定されるがフラットに置くとディレクトリ構成がカオスになる。
また、前提になるPage Routerと同じような使い方をしたかったのでRoute Groupsを使ってルーティングの起点になるディレクトリ((pages)ディレクトリ
)を作成した
まとめ
既存で運用しているディレクトリ構成を骨子に、フェーズに合わせて下記にするのが良いかもしれない。実際に案件で使ったことがないので予想でしかないが。
- srcディレクトリ = appディレクトリ ← 移行案件(今回のサンプルの例)
- pagesディレクトリ = appディレクトリ ← フルスクラッチ案件
補足
Learn Next.jsのfinal-example(完成品)を覗いてみたがcomponentsやfunctionsをappディレクトリの中に配置していたのでVercelはsrcディレクトリ = appディレクトリとして推そうとしているのかなと思ったり。
また、イコールpagesディレクトリとして置き換える目的ならそもそもappディレクトリという命名にしないだろうし(するならviewsとかscreensとか画面を連想させる単語)appが少し抽象ぽいのと単数なのをみるとイコールsrcディレクトリとしてVercelは考えているのかなと妄想する。
Learn Next.jsは今度ハンズオンでやります。
公式からマイグレーションの案内あった
補足2
以下の記事を見てコロケーションでなくroute groupでルーティングは分けたほうが良さそうだなと改めて思った
Next.js App RouterでランタイムCSS-in-JSがNGな理由
CSS Modulesでいく。理由・背景は下記に記載済み。
CSS Modulesを選定した背景でもある「Next.js App RouterでランタイムCSS-in-JSがNGな理由」について今のうちに言語化できるようにしておきたい。大体は下記で済むと思うがエビデンス集めも兼ねて。
メモ
- assets以下にITCSSを展開して骨子になるSassファイルを格納し疎結合にする
- CSS Modulesに関してはtsxファイルと密結合にする
- 動的なスタイルに関してはdata属性でカバーする
- 個人的な趣味としてRSCSSのvariantも導入し運用しやすくする
Next.js App RouterでランタイムCSS-in-JSがNGな理由(続き)
調査不足もあるが「どうしてRSCでCSS-in-JSが使用できないのか」を明文化されているピンポイントな記事が見つけられなかった。
一旦分かったことを書いていく(ここはギリギリまで調べておきたい)
CSS-in-JS
runtime CSS-in-JS allowed developers to express CSS entirely in JavaScript. It was powerful, but the need to inject styles into the DOM at runtime led to performance issues. Moreover, these libraries were incompatible with React Server Components (RSC).
Kuma UIのドキュメントのbut the need to inject styles into the DOM at runtime led to performance issues.
の補足。
Reactの公式ドキュメントにおいても以下の2つの理由からランタイムでstyleタグ挿入を行うCSS-in-JSライブラリは非推奨とされています。
CSS生成のためにブラウザ上でJavaScriptが実行されるため、ページのパフォーマンスへ影響しうる懸念があります。
ちなみに同記事にSSR(Emotion)の挙動が記載されていた
SSR時には初期表示用のCSSをEmotionが作るわけですが、このCSSは .css ファイルとしてブラウザに届くのではなく Next.jsから配信されるHTMLに埋め込まれた状態でブラウザに届きます。
- CSS-in-JSはクライアントサイドのJavaScriptランタイムでスタイルを生成する(
inject styles into the DOM
) - RSCのJSバンドルは排除されるのでスタイルをHTMLに埋め込む等、上記のSSR(Emotion)のようにサポートされないとRSCでCSS-in-JSが使えないのは妥当だと思う
ゼロランタイムCSS-in-JS
逆にゼロランタイムCSS-in-JSならどうなるのか寄り道する
"ゼロランタイム"とは、CSS-in-JSの構文でスタイルを作成するが、生成されるのは他のCSSプリプロセッサが生成するような.cssファイルであるCSS in JSのこと。
ビルド時にCSS in JSのコードからCSSを抽出し、ブラウザがこれらのスタイルを読み込んでウェブページに適用するので、最終的にスタイルタグを生成する際に通常浪費されるランタイムが節約される。
Kuma UI is designed to deliver top-notch performance by eliminating both parsing and injecting of CSS during runtime. It does this by generating CSS ahead-of-time during the build process, ensuring that no runtime overhead is incurred.
上記のIt does this by generating CSS ahead-of-time during the build process, ensuring that no runtime overhead is incurred.
の補足
ランタイムオーバーヘッドのないCSS-in-JSライブラリを作るためには、ビルドタイムでJavaScriptの字句・構文解析及び変換処理を行う必要があります。
LinariaのようなCSS-in-JSライブラリはビルドプロセスでASTからCSSオブジェクトを抽出・変換し最終的なバンドラに含めることでゼロランタイムでの動作を実現しているのです。
- ビルドタイムで.cssファイルを作成するのでランタイムが必要ない
- JSバンドルには含まれないのでRSCと共存できるのか
React Server Component
念の為RSCの補足
This new paradigm introduces a new type of component, Server Components. These new components render exclusively on the server. Their code isn't included in the JS bundle, and so they never hydrate or re-render.
この新しいパラダイムは、新しいタイプのコンポーネント、サーバー・コンポーネントを導入します。この新しいコンポーネントは、サーバー上だけでレンダリングします。このコンポーネントのコードは JS バンドルに含まれないため、ハイドレートや再レンダリングは行われません。
まとめ
調査からRSCでCSS-in-JSがNGな理由がわかった気がする。
Next.js SSRでCSS-in-JSが使用できていたこともあるのでNext.jsのドキュメントではCSS-in-JS libraries which require runtime JavaScript are not currently supported in Server Components.
のように「使えない」とは断言していないのだと思った。(なのでnot currently supported
と名言されている)
ただ、ReactのRSCはランタイムでstyleタグ挿入を行うCSS-in-JSライブラリは非推奨
という背景もあるのでサポートに前向きではないように感じるので今後どう動いていくかは不明。
補足
UIフレームワークとかCSS-in-JSの動向など
Mantine
Chakra UI
↓
MUI
参考記事
今回の趣旨と異なるかもしれないがメモする
レイアウトシフト問題
CourseList
とTestimonials
両方にAPIコールがある場合、ネットワークの速度、待ち時間、その他多くの要因に依存するため、応答が戻ってくる順序については保証がない
function Course() {
return(
<CourseWraper>
<CourseList /> // apiコール1
<Testimonials /> // apiコール2
</CourseWraper>
)
}
ウォーターフォール問題
CourseWraper
もAPIコールをする場合、親コンポーネントはネットワーク呼び出しが完了するまでレンダリングしないと。(子コンポーネントのレンダリングも保持される)
function Course() {
return(
<CourseWraper> // apiコール1
<CourseList /> // apiコール2
<Testimonials /> // apiコール3
</CourseWraper>
)
}
パフォーマンスコスト問題
後で肉付けする
参考記事
こちらも今回の趣旨と異なるかもしれないが良さそうな記事だったのでメモする
SSRの概要
A server generates the initial HTML so that users don't have to stare at an empty white page while the JS bundles are downloaded and parsed. Client-side React then picks up where server-side React left off, adopting the DOM and sprinkling in the interactivity.
RSCの特徴
The key thing to understand is this: Server Components never re-render. They run once on the server to generate the UI. The rendered value is sent to the client and locked in place. As far as React is concerned, this output is immutable, and will never change.*
- Server Componentsは決して再レンダリングしない
- UIを生成するためにサーバー上で一度実行される
- レンダリングされた値はクライアントに送信され、そのまま固定される
これが関数コンポーネントは非同期にはできない(レンダリングに直接副作用を持たせることは許されない)のにServer Componentsはできる理由になる?
This means that a big chunk of React's API is incompatible with Server Components. For example, we can't use state, because state can change, but Server Components can't re-render. And we can't use effects because effects only run after the render, on the client, and Server Components never make it to the client.
- ReactのAPIの大部分がServer Componentsと互換性がない
- 具体的にいうとステートは変更できるのにServer Componentsは再レンダリングできないから
This new paradigm introduces a new type of component, Server Components. These new components render exclusively on the server. Their code isn't included in the JS bundle, and so they never hydrate or re-render.
- Server Componentsのコードは JS バンドルに含まれないため、ハイドレートや再レンダリングは行われない
Server ComponentsとClient Componentsの実装はバウンダリーを意識する
This means we don't have to add 'use client' to every single file that needs to run on the client. In practice, we only need to add it when we're creating new client boundaries.
- クライアント上で実行する必要のあるファイルすべてに'use client'を追加する必要はない
- 実際には、新しいクライアント・バウンダリーを作成するときだけ追加すればok
メモ:SCで作っているつもりが実はCCになっている可能性もあるのでserver onlyで確実にSCの担保をした方がチーム開発では安心できるかもしれない
バウンダリーに関しては以下で説明されていた
RSCを採用するメリット
The most obvious benefit is performance. Server Components don't get included in our JS bundles, which reduces the amount of JavaScript that needs to be downloaded, and the number of components that need to be hydrated:
- サーバーコンポーネントはJSバンドルに含まれないため、ダウンロードするJavaScriptの量が減り、ハイドレーションするコンポーネントの数も減る
This is the sort of thing that gets me excited about React Server Components. Things that would be too cost-prohibitive to include in a JS bundle can now run on the server for free, adding zero kilobytes to our bundles, and producing an even better user experience.
- どこかのサンプルでdate-fnsで書かれていた。(後で探す)
- SCで書くのでdate-fnsのJSバンドル分省略することができる
compositionとSC・CCのInterleaving
- 味噌はvisual hierarchyとdirect dep(すごくわかりやすい...)
- direct depの側面で見ると
Moving Client Components to the Leaves
ができていることが理解できる
- 上記図をさらに保管する動画が次になる。direct depをholeとして説明している
SC→SSができない理由
Since Client Components are rendered after Server Components, you cannot import a Server Component into a Client Component module (since it would require a new request back to the server). Instead, you can pass a Server Component as props to a Client Component. See the unsupported pattern and supported pattern sections below.
- クライアントコンポーネントはサーバコンポーネントの後にレンダリングされるため、サーバコンポーネントをクライアントコンポーネントモジュールにインポートすることはできません(サーバへの新しいリクエストが必要になるため)
compositionでSC→SSができる理由
<ClientComponent> doesn't know that children will eventually be filled in by the result of a Server Component. The only responsibility <ClientComponent> has is to decide where children will eventually be placed.
- <ClientComponent>は、子コンポーネントが最終的にサーバーコンポーネントの結果によって埋められることを知りません。<ClientComponent>が負う唯一の責任は、最終的に子コンポーネントがどこに配置されるかを決定することです。
SC・CCをInterleavingするためのcompositionとパフォーマンス観点のcomposition
過去のkey-frontでパフォーマンス観点でcompositionを利用する内容に触れた。SC・CCをInterleavingするためだけにcompositionは存在しているわけではない。
ドキュメントにも「知っておいて損はない」項目で明記されている
- The pattern of "lifting content up" has been used to avoid re-rendering a nested child component when a parent component re-renders.
- You're not limited to the children prop. You can use any prop to pass JSX.
参考記事
まとめ
長くなりそうなので一旦ここで区切る。今回のスクラップで以下について調査した。
- Next.js App Routerのディレクトリどうするか問題
- Next.js App RouterでランタイムCSS-in-JSがNGな理由
- compositionとSC・CCのInterleaving
あとはkey-front当日に発表して本スクラップに議事録を投げる。
議事録_20231124
- 11/24(金)に実施
- 結論を出すコンテンツではなかったため特に結論はなし
- 参加人数は5名(以下エビデンス)
render-propsでCC→SCができない
render-propsでサバコンを渡せないかと思ったがダメだった。
理由は以下の記事に書いてある。一応素振り内容をメモする。
composition(children props)
compositionに関しては以下で触れている
import ClientComponent from './ClientComponent'
import ServerComponent from './ServerComponent'
export function Composition() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
props-drilling
composition = children propsがokならprops drilling(children以外のpropsで小コンポーネントに流す)でも問題ない
import ClientComponent from './ClientComponent'
import ServerComponent from './ServerComponent'
export function PropsDrilling() {
return <ClientComponent propsDrilling={<ServerComponent />} />
}
render-props
render-propsはつまるところ関数を渡している。
- Server ComponentからClient Componentに渡されるProp値はシリアライズ可能でなければならない。
- okな例)Client or Server Component elements (JSX)
- ngな例)Functions that are not exported from client-marked modules or marked with 'use server'
クラコンに渡すことができるのが関数だとServer Action(marked with use server)だけなのでrender-propsだとエラーになる
import ClientComponent from './ClientComponent'
import ServerComponent from './ServerComponent'
export function RenderProps() {
return <ClientComponent render={() => <ServerComponent />} />
}
参考記事
composition関連の積読があったので消化する
compositionでCC -> SCができる理由
The reason this works is that, while we’re building a tree where the server components are rendered within the client component, the component doing the importing is a server component itself, so we’re never breaking the “server components can only import other server components” rule.
厳密にはSC内でcompositionしているのが味噌になるのか。server components can only import other server components
の部分。
以下は先ほどのrender-propsの項目で触れた内容と同じになるが念の為補足
- インポートするコンポーネントがSCである限り(以下の例で言うとpage.tsxがSCである限り)レンダリングされたSCをcomposition(children props)または通常のpropsとして渡すことができる
- CCはレンダリングされていないコンポーネントをpropsとして渡して小コンポーネントでレンダリングさせることもできるがSCの場合はNG
// ok
<ClientCard footer={<ServerFooter />} />
// ng
<ClientCard Footer={ServerFooter} />
先ほどのrender-propsの項目でrender-propsでCC -> SCはできないと書いたがどうやらできるっぽい。言われてみると確かに可能ではあるよなという実装だった(ハッキーな実装になるのと、記事にも推奨はしていないようです。引用すると誤解されてしまう可能性があるので気になる方だけ表題の記事をみてください。)
以下の積読を消化したのでメモ。
気になったところだけ引用で抜粋して最後に感想だけ。
The idea of "client components" rendering on the server might seem confusing but it's helpful to view them as components that primarily run on the client but can (and should) also be executed once on the server as an optimization strategy.
- クラコンはサーバーでも実行される。
- 確かTwitterでクラコンにconsole.log仕込んだらブラウザだけでなくnode環境でも標準出力されるってコメントがあってそれに対してのアンサーになりそうだと思ったのでメモ
Third, Server Components' exclusive server-side execution enhances security by keeping sensitive data and logic, including tokens and API keys, away from the client-side.
- サバコンを使うことでセキュリティが強化される
With React Server Components architecture, Server Components take charge of data fetching and static rendering, while Client Components are tasked with rendering the interactive elements of the application.
- サバコンはデータ取得と静的レンダリングを担当し、クラコンはインタラクティブ要素のレンダリングを担当する
感想
少し長め、かつ英語の記事になるので読むのに時間がかかるがSPAからRSCまでの変遷とアニメーションを使った図解がわかりやすく、RSCに関してはレンダリングフローまで載っていて、ここまで体系的にまとまっているのは見なかったので良い記事だと思った。
上記の記事の内容は実はすでに以下の3つのスクラップでまとめている。が脱線しまくりだし箇条書きで読みにくい部分も多々あるので一気にNext.jsを理解したいなら上記の記事をみるのがおすすめです。
[MEMO]
ランタイムとゼロランタイムのCSS生成の説明がわかりやすかったのでメモ
12pと13pに記載あり