📑

<Card.Root> のように実装すべきなのか

2024/09/27に公開

はじめに

<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の文脈で問題が発生します。
例えば、以下のようなコードがあるとします。

components/card.tsx
function Card() {}

Card.Header = function Header() {}
Card.Body = function Body() {}
Card.Footer = function Footer() {}

export default Card;
example/card.tsx
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タグを含んだオブジェクト)。そのため、エラーが発生するでしょう。
https://github.com/facebook/react/blob/473522093d3dd95582729d01cd5c0d15dcc9cd3b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js#L188-L192

シリアライズの問題が発生するため、以下のissueで議論されていました。

RSCの文脈でdot notationをする代替案

RSCの文脈では関数はシリアライズできないため、Cardオブジェクトとしてexportします(export * as Card)。

コンポーネントをnamed exportすれば、モジュールレベルでtree shakingしやすいでしょう。
https://github.com/chakra-ui/chakra-ui/blob/d4f9d312da0144c081cdb48021bc6396f3290bc6/packages/react/src/components/card/card.tsx#L30-L33

コンポーネントをre-exportするときに、aliasを使うことで、tree-shakingを保ちながら、命名を簡潔にすることができます。
https://github.com/chakra-ui/chakra-ui/blob/d4f9d312da0144c081cdb48021bc6396f3290bc6/packages/react/src/components/card/namespace.ts#L1-L9

https://github.com/chakra-ui/chakra-ui/blob/d4f9d312da0144c081cdb48021bc6396f3290bc6/packages/react/src/components/card/index.ts#L21

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では、以下のルールを設定すればバンドルサイズを抑えれるかもしれません。

GitHubで編集を提案

Discussion