Closed10

【key-front】Next.js App Routerの素振り(ディレクトリ構成・CSS選定・Compositionパターン)

1zushun1zushun

モチベーション

  • 毎週木曜日Slackのkey_frontチャンネルでハドル機能を使いお題に対してメンバー同士ディスカッションをする時間を15〜30分程度設けている
  • 今回は個人開発を通して学んだNext.js App Routerについて取り上げる
  • ファシリテーターは筆者なので、事前に読み込んで気になった点などをスクラップに投げていく
  • 開催日は11/24(金)で最終的に議事録として結論をまとめる

過去に開催した関連してそうな内容

https://zenn.dev/shuuuuuun/scraps/7b2aaf746aec91

1zushun1zushun

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の所感
1zushun1zushun

Next.js App Routerのディレクトリどうするか問題

  • ルーティングが期待値になっていることは確認済み

前提

  • Next.js12からNext.js13(App Router)へ移行する
  • リポジトリルートにsrcディレクトリ(既存アプリのソース)がある
  • Page Routerみたいにルーティングさせたい

appディレクトリ以下に移行済みのソースを配置する

前提を踏まえappディレクトリ以下にもcomponentsやその他ディレクトリを用意し移行済みのファイルを配置すると丸いのでは?と思ったので添付画像の構成にした。

少なくともappディレクトリ以下ならuse clientなどApp Router独自の宣言などが書かれていても不思議ではないし、appディレクトリ以下でだけSC・CCの関係性を考えれば良くなり余計な心配をする必要がなくなる。

次の記事が勉強になりました。

https://zenn.dev/brachio_takumi/articles/5af43549cdc4e0

Private Folders

appディレクトリ以下でPrivate Foldersは使用していない。理由としては(pages)ディレクトリ以下がルーティングの責務であることが明確なため。

https://nextjs.org/docs/app/building-your-application/routing/colocation#private-folders

Route Groups

appディレクトリ以下はpage.tsxであればルーティング判定されるがフラットに置くとディレクトリ構成がカオスになる。

また、前提になるPage Routerと同じような使い方をしたかったのでRoute Groupsを使ってルーティングの起点になるディレクトリ((pages)ディレクトリ)を作成した

https://nextjs.org/docs/app/building-your-application/routing/colocation#route-groups

https://nextjs.org/docs/app/building-your-application/routing/route-groups

まとめ

既存で運用しているディレクトリ構成を骨子に、フェーズに合わせて下記にするのが良いかもしれない。実際に案件で使ったことがないので予想でしかないが。

  • 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は今度ハンズオンでやります。

https://nextjs.org/learn

https://github.com/vercel/next-learn/tree/main/dashboard/final-example

https://nextjs.org/docs/app/building-your-application/routing/colocation

公式からマイグレーションの案内あった

https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration

1zushun1zushun

Next.js App RouterでランタイムCSS-in-JSがNGな理由

CSS Modulesでいく。理由・背景は下記に記載済み。

https://zenn.dev/shuuuuuun/scraps/744aa994686183

CSS Modulesを選定した背景でもある「Next.js App RouterでランタイムCSS-in-JSがNGな理由」について今のうちに言語化できるようにしておきたい。大体は下記で済むと思うがエビデンス集めも兼ねて。

https://zenn.dev/shuuuuuun/scraps/7b2aaf746aec91

メモ

  • assets以下にITCSSを展開して骨子になるSassファイルを格納し疎結合にする
  • CSS Modulesに関してはtsxファイルと密結合にする
  • 動的なスタイルに関してはdata属性でカバーする
  • 個人的な趣味としてRSCSSのvariantも導入し運用しやすくする
1zushun1zushun

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).

https://www.kuma-ui.com/docs#from-runtime-css-in-js-to-zero-runtime-css-in-js

Kuma UIのドキュメントのbut the need to inject styles into the DOM at runtime led to performance issues.の補足。

Reactの公式ドキュメントにおいても以下の2つの理由からランタイムでstyleタグ挿入を行うCSS-in-JSライブラリは非推奨とされています。

https://zenn.dev/poteboy/articles/e9f63b87b3cd69#ランタイムcss-in-jsの問題点

CSS生成のためにブラウザ上でJavaScriptが実行されるため、ページのパフォーマンスへ影響しうる懸念があります。

ちなみに同記事にSSR(Emotion)の挙動が記載されていた

SSR時には初期表示用のCSSをEmotionが作るわけですが、このCSSは .css ファイルとしてブラウザに届くのではなく Next.jsから配信されるHTMLに埋め込まれた状態でブラウザに届きます。

https://techlife.cookpad.com/entry/2023/10/03/105240

  • 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を抽出し、ブラウザがこれらのスタイルを読み込んでウェブページに適用するので、最終的にスタイルタグを生成する際に通常浪費されるランタイムが節約される。

https://zenn.dev/takuyakikuchi/articles/b1b20f65d4f9cf#ゼロランタイム-css-in-js

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.

https://blog.bitsrc.io/i-created-a-zero-runtime-css-in-js-library-compatible-with-next-js-app-router-and-rsc-9c016e3c1ce2

上記の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オブジェクトを抽出・変換し最終的なバンドラに含めることでゼロランタイムでの動作を実現しているのです。

https://zenn.dev/poteboy/articles/e9f63b87b3cd69#ゼロランタイムcss-in-jsの仕組み

  • ビルドタイムで.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 バンドルに含まれないため、ハイドレートや再レンダリングは行われません。

https://www.joshwcomeau.com/react/server-components/#introduction-to-react-server-components-3

まとめ

調査から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と名言されている)

https://nextjs.org/docs/app/building-your-application/styling/css-in-js

ただ、ReactのRSCはランタイムでstyleタグ挿入を行うCSS-in-JSライブラリは非推奨という背景もあるのでサポートに前向きではないように感じるので今後どう動いていくかは不明。

補足

UIフレームワークとかCSS-in-JSの動向など

Mantine

https://github.com/mantinedev/mantine/issues/2815#issuecomment-1509758301

Chakra UI

https://zenn.dev/osayubot/articles/1fbbb5415e3a5d

https://blog.stin.ink/articles/chakraui-is-still-client-components

MUI

https://mui.com/material-ui/guides/next-js-app-router/

https://github.com/mui/material-ui/issues/38137

参考記事

https://www.wantedly.com/companies/wantedly/post_articles/406209

https://zenn.dev/takecchi/scraps/48fd0a3da275b5

https://postd.cc/stop-using-css-in-javascript-for-web-development-fa/

1zushun1zushun

今回の趣旨と異なるかもしれないがメモする

レイアウトシフト問題

CourseListTestimonials両方にAPIコールがある場合、ネットワークの速度、待ち時間、その他多くの要因に依存するため、応答が戻ってくる順序については保証がない

sample1
function Course() {
 return(
     <CourseWraper>
 		<CourseList /> // apiコール1
 		<Testimonials /> // apiコール2
     </CourseWraper>
 )
}

ウォーターフォール問題

CourseWraperもAPIコールをする場合、親コンポーネントはネットワーク呼び出しが完了するまでレンダリングしないと。(子コンポーネントのレンダリングも保持される)

sample2
function Course() {
 return(
     <CourseWraper> // apiコール1
 		<CourseList /> // apiコール2
 		<Testimonials /> // apiコール3
     </CourseWraper>
 )
}

パフォーマンスコスト問題

後で肉付けする

参考記事

https://www.freecodecamp.org/news/how-to-use-react-server-components/

1zushun1zushun

こちらも今回の趣旨と異なるかもしれないが良さそうな記事だったのでメモする

https://www.joshwcomeau.com/react/server-components

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の担保をした方がチーム開発では安心できるかもしれない

https://www.npmjs.com/package/server-only

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バンドル分省略することができる
1zushun1zushun

compositionとSC・CCのInterleaving

  • 味噌はvisual hierarchyとdirect dep(すごくわかりやすい...)
  • direct depの側面で見るとMoving Client Components to the Leavesができていることが理解できる

https://twitter.com/honey321998/status/1628644758175481858

https://qiita.com/honey32/items/bc24d8c0ea3d096ff956

  • 上記図をさらに保管する動画が次になる。direct depをholeとして説明している

https://twitter.com/asidorenko_/status/1633930160561963012

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は存在しているわけではない。

https://zenn.dev/link/comments/99636c03afc737

ドキュメントにも「知っておいて損はない」項目で明記されている

  • 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.

参考記事

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns

https://zenn.dev/frontendflat/articles/nextjs-rscpayload-composition

1zushun1zushun

まとめ

長くなりそうなので一旦ここで区切る。今回のスクラップで以下について調査した。

  • Next.js App Routerのディレクトリどうするか問題
  • Next.js App RouterでランタイムCSS-in-JSがNGな理由
  • compositionとSC・CCのInterleaving

あとはkey-front当日に発表して本スクラップに議事録を投げる。

1zushun1zushun

議事録_20231124

  • 11/24(金)に実施
  • 結論を出すコンテンツではなかったため特に結論はなし
  • 参加人数は5名(以下エビデンス)
このスクラップは5ヶ月前にクローズされました