<Card.Root> のように実装すべきなのか
はじめに
<Card.Root>
のような記述を見かけることがあります。ですがReact Server Component(以下、RSCという)の文脈では、<Card.Root>
のような記述は避けるべきとされています。なぜでしょうか。
<Card.Root>
は"dot"を使用してオブジェクトのプロパティにアクセスする方法を指します。dot notationと呼ばれる記法です。dot notationに関するissueを見かけたので、何が起きているのかを調べてみました。
代表的なdot notationの例
例えば、React Contextを使ったとき、ThemeContext
というトップレベルコンポーネントがあり、それに関連する Provider
というサブコンポーネントがThemeContext.Provider
のようにdot表記で参照されるのです。
import { createContext, useContext } from 'react';
const Theme = createContext(null);
function App({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(Theme);
return <header style={{ color: theme === 'dark' ? 'white' : 'black' }}>Header</header>;
}
dot notationで実装をすると何が起こるのか
dot notationで実装をすると、RSCの文脈で問題が発生します。
例えば、以下のようなコードがあるとします。
function Card() {}
Card.Header = function Header() {}
Card.Body = function Body() {}
Card.Footer = function Footer() {}
export default Card;
import { Card } from "../components/card"
export const BasedCard = () => {
return (
<Card>
<Card.Body>
</Card.Body>
<Card.Footer justifyContent="flex-end" gap="2">
</Card.Footer>
</Card>
)
}
このとき、シリアライズの問題が発生します。関数はJSONにシリアライズできないため、RSCと互換性がありません(シリアライズできるのは、"div" などの基本的なHTMLタグを含んだオブジェクト)。そのため、エラーが発生するでしょう。
シリアライズの問題が発生するため、以下のissueで議論されていました。
- "Dot notation client component breaks consuming RSC"
- "This is probably a bug in the React Server Components bundler"
RSCの文脈でdot notationをする代替案
RSCの文脈では関数はシリアライズできないため、Card
オブジェクトとしてexportします(export * as Card
)。
コンポーネントをnamed exportすれば、モジュールレベルでtree shakingしやすいでしょう。
コンポーネントをre-exportするときに、aliasを使うことで、tree-shakingを保ちながら、命名を簡潔にすることができます。
re-export, index.ts(Barrel file), namespace import を使用することに抵抗感を持つ人もいるかもしれません。PreactのメンバーであるMarvin Hagemeisterの記事 "Speeding up the JavaScript ecosystem - The barrel file debacle" でも、デメリットが述べられていました。
In a real project these numbers are likely worse. Barrel files are not good when it comes to tooling performance.
re-exportをするモジュール数が増加するとバンドルされるコードも増加しそうです。上記のコンポーネントに対してデメリットを挙げてきましたが、個人的には、丁寧に良く書かれたモジュールはre-exportされてもtree-shakingされると考えていて、re-exportされたモジュールを使うことに抵抗感はありません。
ですが、dot notationを使用するときの欠点なのかもしれません。コンポーネントの一部だけをimportしたいときは、CardRoot
を直接importすることを強く推奨されるのではないでしょうか。
import { CardRoot, CardBody } from "@/components/ui/card";
export function App() {
return (
<CardRoot>
<CardBody>BODY</CardBody>
</CardRoot>
);
}
余談としてBiomeでは、以下のルールを設定すればバンドルサイズを抑えれるかもしれません。
Discussion