Kuma UI はどのように React Server Components をサポートしているのか
はじめに
こんにちは。READYFOR でフロントエンドエンジニアとして働いている菅原(@kotarella1110)です!
私は OSS が大好きで、React Hook Form や Kuma UI のメンテナとしても活動しています。
弊社のプロダクトでは CSS ライブラリとして Emotion を採用していますが、ランタイムでのパフォーマンス上の問題や App Router 非対応等の理由から、ゼロランタイム CSS ライブラリへの移行を検討しています。このような背景から、ゼロランタイム CSS に関する話題が社内で頻繁に取り上げられています。
そこで本記事では、その中でも私がメンテナとして関わっている Kuma UI がどのように React Server Components をサポートしているかを詳しくご紹介します。
Kuma UI とは?
Kuma UI は、新しい手法である Hybrid Approach を採用した CSS in JS ライブラリです。
この手法では、静的に解析可能なスタイルはビルド時に抽出し、動的に変化するスタイルだけを JavaScript のランタイムで処理します。
これにより、Chakra UI などの従来型のランタイム CSS-in-JS の書き味を維持することを実現しています。
import { useReducer } from "react";
import { Box, Text, Button } from "@kuma-ui/core";
function App() {
const [checked, toggle] = useReducer((state) => !state, false);
return (
<Box>
<Text
fontSize="24px" // 静的に解析可能なためゼロランタイムで処理
color={checked ? "red" : "blue"} // 動的に変化する値のためランタイムで処理
>
Hello World
</Text>
<Button onClick={toggle}>Click Me</Button>
</Box>
);
}
Kuma UI の詳細については Kuma UI の作者である poteboy さんの記事を、
Hybrid Approach の詳細については、メンテナである yuku さんの記事をご覧ください。
「React Server Components のサポート」の定義
先ず、言葉の定義を確認しておきましょう。
CSS in JS ライブラリが Next.js の App Router をサポートしているからといって、Server Components として動作するとは限りません。
以下は、 Next.js の公式ドキュメントにおいて App Router に対応した CSS-in-JS ライブラリの一覧が示されています。
この一覧には、styled-components や Material UI といったランタイム CSS in JS ライブラリが含まれていますが、これらは Client Components としてのみ動作します。例えば、Material UI v5 では、全てのコンポーネントに "use client"
ディレクティブを含めることで、Next.js の App Router をサポートしていています。[1]
本記事での「React Server Components のサポート」とは、CSS in JS ライブラリが Server Components として動作することを指しています。
余談ですが、Next.js の公式ドキュメントでは Client Components のサポートについてのみが強調されており、Server Components のサポートに関しては触れられていません。また、CSS-in-JS ライブラリの一覧の中には Server Components をサポートしているライブラリも存在する一方で、これが混乱を招く可能性があるため、最近 poteboy さんが以下のようなツイートをしています。
何故ランタイム CSS ライブラリは React Server Components をサポートできないのか?
では、何故ランタイム CSS ライブラリは Server Components をサポートできないのでしょうか?
結論から言うと、Server Components で DOM などのブラウザ専用 API は使用できないためです。
ランタイム CSS ライブラリは、JS で記述されたスタイルから CSS ルールを生成し、それらをドキュメントに style
タグで挿入します。
しかし、Server Components では DOM 操作が不可能なため、これらのライブラリは Server Components として動作させることができません。
以下のコードを見てもらえれば、Kuma UI のランタイム処理でも同様に style
タグで挿入していることが分かると思います。[2]
Kuma UI も React Server Components をサポートしていないのでは?
ここで「Kuma UI は動的なスタイルを含む場合にランタイムで処理するため、Server Component として動作しないのでは?」という疑問が生まれてくると思います。
これは半分正解で、半分誤りです。
正確には、 「静的なスタイルのみの場合は Server Component として動作するが、動的なスタイルを含む場合のみ Client Component として動作する」 と言えます。
その理由は、動的なスタイルを含む場合に Client Component に切り替わるためです。次のセクションで、詳細を見ていきましょう。
Kuma UI はどのように React Server Components をサポートしているのか
Kuma UI の Flex
や Button
といった全てのコンポーネントは、ビルド時に Box
コンポーネントに変換されます。
そのため、Box
コンポーネントは Hybrid Approach の中核を担う重要なコンポーネントとなっています。
Box
コンポーネントの実装を確認することで、Server Components への対応内容が明確になります。
対応内容は非常にシンプルです。Box
コンポーネントは、指定された props がすべて静的解析可能な場合には "use client"
ディレクティブを含まない StaticBox
というコンポーネントを返し、静的解析できない動的な props が存在する場合には "use client"
ディレクティブを含む DynamicBox
という Client Component を返します。
また、補足として、StaticBox
は Server Component にも Client Component にもなり得ます。どちらになるかは、Box
を import しているコンポーネントに "use client"
ディレイクティブの境界があるかないかに依存します。
つまり、Box
コンポーネントはゼロランタイムとランタイムの切り替えという面だけでなく、Server Component と Client Component の切り替えという面でも Hybrid なアプローチを取っています。
コンポーネントを配信する上での注意点
これは主にライブラリ開発者を対象とした情報ですが、ライブラリ全体を "use client"
ディレクティブで宣言するのではなく、Kuma UI のように特定のファイルでのみ "use client"
ディレクティブを宣言して配信する場合、一つのファイルをバンドルせずに配信する必要があります。
説明を分かりやすくするために、Client Components を含むコンポーネントライブラリをバンドルした結果を以下に仮定します。[3]
function RSC() {
return <div>React Server Component</div>;
}
"use client";
import { useEffect } from "react";
function RCC() {
useEffect(() => {}, []);
return <div>RCC</div>;
}
export { RSC, RCC };
"use client"
ディレクティブはファイルの先頭に配置する必要があり、このコンポーネントライブラリを App Router などで import すると、以下のエラーが発生します。
The "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.
ちょっと雑な例となってしまいましたが、tsup 等のビルドツールでは、"use client"
のようなディレクティブは削除されるため、その結果、Server Component でのフック呼び出し違反等の別のエラーが発生します。Kuma UI でも、tsup を使用しており、これに対処するために、以下のように tsup の設定でファイルをバンドルしないようにしています。このように設定することで、"use client"
ディレクティブも削除されず、ファイルが分割されたまま配信され、期待通りの動作をするようになります。
import { defineConfig } from "tsup";
export default defineConfig({
entry: ['src/index.ts'],
- bundle: true,
+ bundle: false,
});
Kuma UI の React Server Components サポートへの展望
本記事を執筆する中で、動的なスタイルが含まれている場合でも Server Components として動作させる方法を検討しました。 果たしてそれは可能なのでしょうか?
突然ですが、皆さん CSS Hooks という最近登場し注目されている CSS in JS ライブラリをご存知でしょうか?
インラインスタイル(style
prop)によるスタイリングだと hover や media query を使用したレスポンシブな動作の表現が不可能です。これらの技術的な制限を、CSS のテクニックを用いることで克服し、理想的なインラインスタイルによるスタイリングを可能にするライブラリが CSS Hooks です。
以下は CSS Hooks を使った hover エフェクトが実現されたボタンの例です。
import { createHooks } from "@css-hooks/react";
const [hooks, css] = createHooks({
"&:hover": "&:hover",
});
export default function App() {
return (
<>
<style dangerouslySetInnerHTML={{ __html: hooks }} />
<button
style={css({
color: "red",
"&:hover": {
color: "blue",
},
})}
>
Hover me
</button>
</>
);
}
CSS Hooks の API の返り値を展開すると、以下のようになります。[4]
export default function App() {
return (
<>
<style>
* {
--hover-off: initial;
--hover-on: ;
}
*:hover {
--hover-off: ;
--hover-on: initial;
}
</style>
<a
a
style={{
color: "var(--hover-on, blue) var(--hover-off, red)"
}}
>
Hover me
</button>
</>
);
}
コードを確認すると、以下の2つの特別な値とCSS変数のフォールバックを活用して、先に述べた技術的な制限を克服しています。
値 | 説明 | 例 |
---|---|---|
initial |
無効な値。この値を持つ CSS 変数を参照すると、代わりにフォールバック値が使用されます。 | --foo: initial; |
空の値 | 有効な値。この値を持つ CSS 変数を参照すると、空の値が使用されます。フォールバックされません。 | --foo: ; |
hover エフェクトにより、--hover-off
と --hover-on
変数の状態を制御するだけで、インラインスタイル内で定義された任意の値を切り替えています。これにより、インラインスタイル内でのスタイリングを柔軟に実現しています。
CSS Hooks について言及しましたが、察しの良い方はその理由について既にお気づきかもしれません。
そうです、動的なスタイルが含まれている場合のみ CSS Hooks と同じ手法を用いることで Server Components として動作させることができるということです。この手法には、Server Components を完全にサポートする以外にも以下のメリットがあります。
- SSR のパフォーマンス向上: ダブルレンダーが不要になり、シングルレンダーにすることで SSR のパフォーマンスが向上します。styled-components や styled-jsx などと同様に、Kuma UI は SSR で一般的に2回レンダリングが必要です。[5] 1回目はスタイルを収集し、2回目は1回目で収集したスタイルから生成した style タグを head に埋め込みつつレンダリングします。
- ランタイム処理の最小化: DOM 操作による style のインジェクションが不要になるため、ランタイム処理が最小限になります。また、やろうと思えばランタイム処理を完全に排除しゼロランタイムに寄せることも可能です。
ただし、いくつかの注意点も存在します。インラインスタイル指定は、Cascade Layersで定義したスタイルよりも高い優先度を持つため、優先度の考慮が必要になります。また、現時点では、インラインスタイルがパフォーマンスに与える影響について正確に把握できていません。
これらの課題に対して慎重な検証と評価を行い、この手法の採用可否を検討していきたいと思います。
おわりに
本記事では、Kuma UI が React Server Components をどのようにサポートしているかを解説し、React Server Components サポートの将来の展望についても紹介しました。
他にも、スプレッド構文以外の動的なスタイルをビルド時に静的解析できるように改善したり、ゼロランタイムな Emotion 完全互換を目指したりする予定[6]です。これらの取り組みにより、Kuma UI は今後も進化し続けます 🐻❄️
現在、Kuma UI は日本人4人のメンテナが中心となって開発されています。また、公式 Twitter や Discord サーバー も運営しておりますので、興味のある方はぜひ参加してください。
記事を読んで Kuma UI の取り組みに少しでも良いなと思った方は、GitHub で ⭐️ をいただけると非常に嬉しいです。これからも Kuma UI の開発を継続していく中で、皆様のサポートが大きな励みとなります。
-
Material UI v5 まではランタイムですが、v6 からゼロランタイムに移行することで Server Components として動作するようになります。 ↩︎
-
理解してもらいやすいように、ブラウザでのみ動作するランタイム処理のコードをお見せしていますが、SSR では Server Components 同様 DOM 操作が出来ないためこちらのコードは実行されません。SSR 側の処理に興味がある方は yuku さんの記事 「SSR でランタイム CSS を反映する」のセクションをご覧下さい。 ↩︎
-
あくまでイメージしやすいように用意したバンドル結果です。実際にバンドルされる結果とは大きく異なります。 ↩︎
-
本来 CSS 変数名はハッシュ化されますが、理解しやすい命名に変更しています。 ↩︎
-
ここで「一般的に」と表現しているのは、Next.js では
useServerInsertedHTML
フックを使用して SSR するため、これによりシングルレンダーで済むように見受けられるためです。実際にレンダリングのログを出力してみましたが、一度のみの出力でした。useServerInsertedHTML
のコードを十分に追及できていないので、もしご存じの方がいれば教えてください。また、Kuma UI はまだ Remix をサポートしておりませんが、Remix をサポートする場合はダブルレンダーが必要とされます。=> koichik さんがダブルレンダーに関して言及してくれました!ありがとうございます!詳細についてはこちらのスレッドをご覧ください。 ↩︎ -
詳細については yuku さんの記事「Hybrid Approach のこれから」のセクションをご覧下さい。 ↩︎
「みんなの想いを集め、社会を良くするお金の流れをつくる」READYFORのエンジニアブログです。技術情報を中心に様々なテーマで発信していきます。 ( Zenn: zenn.dev/p/readyfor_blog / Hatena: tech.readyfor.jp/ )
Discussion