ドーナッツが"use client"を制する鍵?
React Server Components(以下、RSC)の文脈で、「どのコンポーネントをServer ComponentにするのかClient Componentにするのか判断に迷うからひとまずツリーの一番上で"use client"にしちゃえばうんぬん・・・」、ということを風の噂で聞きつけたので、よくあるClient Componentがドーナッツになれる例えを身につけると幸せになれるんじゃないかという話をします。(ドーナッツ?何を言っているんだろう)(もうすでに擦られた内容だとも思いますが、意外と存在しないのかなと詳しく調べもせずに筆を執っています)(要出典)
想定読者は、ある程度はRSCについて聞いたことがある、RSCを触っているけどコンポーネント定義に自身がない、"use client"を付ける判断基準が明確にぱっと言えない、などの方達です。
(ここで書く考え方、個人的には気に入っているんですが、他の人にささるかどうか知りたくて記事を書いてますが、多分雑すぎてフィードバックを得られないでしょう)
一応これらを貼っておきます
はじめに
勢いで描いてツイートしたものがこちらで、これについて少し説明を加えてみようと思います。
お題
まずは画像と文章をもう一度。
このページのコンポーネントツリーをRSCでどう設計しますか?
前提:Next.jsのApp Routerを使います。コンポーネントは"use client"を書かなければserver componentとして扱われます。
このページはタブとボタンを持ちます。ということはこれらはclient componentなので、"use client"が必要です。
あなたはどのようにコンポーネントツリーを設計しますか?
”ドーナッツ”マインドに従うと、自ずと答えが見えてくるでしょう。
さて、ドーナッツのことは一旦置いておき、このページのコンポーネントをRSC抜きにして書いてみるとしたらどうしますか?想像してみてください。
ページ内でコンポーネントの切り分けもなく全てを書く?(いやいやそんなことはしないですよ〜🤭)
ある程度コンポーネントは切り分けて書く?(ん〜、タブの中身は切り出すかな〜🤔)
RSC以前では個人の流儀やプロジェクトの規約に則ってコンポーネントを書いていたかもしれませんが、RSC対応フレームワークを使う場合はServer ComponentとClient Componentを区別してコンポーネントを書く必要があり、悩みのタネが増えた感はありつつも、個人的には仕組みが指針を用意してくれたと思っておりむしろプラスに捉えています。
ただまぁ、腑に落ちて使いこなせないとその恩恵は受けれませんし、パラダイムシフトが起きてるわけで難しく感じるのは当然だと思います。自分も慣れるのには時間がかかりました。
Server Component と Client Componentを色分けしてみよう
お題のコンポーネントを、Server Componentを赤色、Client Componentを青色で色分けすると、大まかには以下のようにできます。
この色分けがスッとできると、もう大丈夫な気もしますがどうでしょう?(理解できる人も)
何故この色分けになるかですが、Client Componentとは簡単にいうと「インタラクションのあるコンポーネント」、「hooksを使っているコンポーネント」です。ということは、タブとボタンをClient Componentで定義します。それ以外はタイトルと文章となり、Server Componentに"なれます"。
おそらく分かれ道は、タブ以下ではないでしょうか。なので以下のように色分けした人もいるかと思います。タブコンポーネント以下全てをClient Componentとする分け方ですね。
この色分けが”間違いではない”ことは抑えておきたいポイントです。
間違いではないのですが、最初に示した色分けが出来るようになると、RSCの恩恵を受けるコードが書けます。
2パターンのタブコンポーネントの違いでドーナッツを感じる
もう上の絵を見てもらったらわかるように、Client Componentは”穴”を開けることができ、"use client"を書いたからと言ってそのコンポーネント(ファイル)以下が全てClient Componentになるわけではなく、その”穴”を通してServer Componentを流し込むことが出来ます。
ここでは、全てをClient Componentで書いたパターン(図の左)と、Server Componentを受け取るパターン(図の右)を例に比較してみようと思います。
左のコードでは、タブの内容(<TabContentA />
)をベタ書きしています。
一方、右のコードでは、タブの内容(props.tabContentA
)をReactNodeとして受け取っています。
Tabsコンポーネントの親がServer Componentである場合、<Tabs tabContentA={<TabContentA />} />
とした場合の<TabContentA />
はClient Componentである必要はなく、Server Componentとして定義可能であり、結果、Tabsのprops.tabContentA
はServer Componentとして扱われます。
"use client"は境界である、というのは間違いではないのですが、その境界"線"を引くと世界が完全に分断されるようにイメージしてしまい、左のツリーを想像してしまうのは仕方がないと思います。
しかしそうではなく、境界に穴を開けることができ、完全に分断しなくて済むことが見て頂けたかと思います。
この、コンポーネントツリーの絵で穴が空いたClient Componentの見た目がドーナッツのようであり、穴を開けるも開けないも自由ということを感じて頂けたでしょうか🍩
さいごに
RSCにおける"use client"を書く勘所として、Client Componentに”穴”を開ける事が可能なこと、その穴を通してServer Componentを流し込めること、このことをドーナッツをイメージして覚えることでServer ComponentとClient Componentの扱いを適切に判断する助けになるかもしれないことを紹介しました。
「穴なしドーナッツも可能ですが、穴ありドーナッツもお作りできますがいかがでしょう?」
ドーナッツマインドでコンポーネントツリーの設計を試してみてください。
お口直しにあっきーさんの記事をどうぞ
P.S. React Tokyoでミーム大会があり、この記事のネタの流れで作ってみたのでここで供養します。
"Server ComponentとClient Componentはちゃんと区別しt・・"
""use client"ってページトップに書けっ!"
ページファイルの1行目に"use client"を書こうと思う日が無くなりますように🍩
Discussion