Interactive

コンポーネント設計

適切なコンポーネント設計パターンを理解することで、ClaudeCodeは再利用性と保守性の高いUIコンポーネントを自動生成できます。弊社では、shadcn/uiをベースとしたデザインシステムとGraphQLコロケーションを重視した設計を採用します。

弊社の推奨ルール

  • UIライブラリはshadcn/uiを使用し、カスタマイズは最小限に留める
  • GraphQL採用時はgql.tadaでコロケーションパターンを実装
  • コンポーネント粒度はデザインシステムに準拠(Card、Button等)
  • useMemo/useCallbackは実測してから適用、早すぎる最適化は避ける
  • Atomic Designは採用せず、デザイン言語を重視

ClaudeCodeでの利用

「shadcn/uiのCardコンポーネントをベースに商品カードを作成」
「このコンポーネントにGraphQLフラグメントをコロケーション」
「ButtonとCardを組み合わせて購入フローのUIを構築」
「パフォーマンス問題が出たらuseMemoの適用箇所を提案」

shadcn/uiを基盤として使う

UIコンポーネントを一から作ると開発コストが増大します。shadcn/uiはコピー&ペースト方式で必要な部分だけ導入でき、カスタマイズも容易です。

// shadcn/uiのコンポーネントをそのまま使用
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export function ProductCard({ product }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{product.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-muted-foreground">{product.description}</p>
        <Button className="mt-4">購入する</Button>
      </CardContent>
    </Card>
  );
}

基本的なUIはshadcn/uiのコンポーネントをそのまま使い、独自実装は避けます。大幅なカスタマイズが必要な場合は、別ディレクトリに複製してcard-customのような名前で管理します。

// 大幅にカスタマイズが必要な場合
components/
  ui/                     // shadcn/uiのオリジナル
    card.tsx
    button.tsx
  custom/                 // カスタマイズ版
    card-custom.tsx       // 独自のカード実装
    table-custom.tsx      // 独自のテーブル実装

GraphQLフラグメントのコロケーション

GraphQLを採用している場合、データ要求をコンポーネントと同じ場所に配置することで、必要なデータが明確になります。

// gql.tadaを使ったコロケーション
import { graphql } from "@/graphql";

// コンポーネントが必要なデータをフラグメントで定義
const ProductCardFragment = graphql(`
  fragment ProductCardData on Product {
    id
    name
    price
    imageUrl
    category {
      name
    }
  }
`);

export function ProductCard({ product }) {
  // フラグメントで型安全にデータアクセス
  const data = useFragment(ProductCardFragment, product);
  
  return (
    <Card>
      <img src={data.imageUrl} alt={data.name} />
      <CardContent>
        <h3>{data.name}</h3>
        <p>{data.category.name}</p>
        <span>¥{data.price.toLocaleString()}</span>
      </CardContent>
    </Card>
  );
}

コンポーネントとデータ要求を一緒に管理することで、オーバーフェッチやアンダーフェッチを防ぎます。

デザインシステムに準拠した粒度とネーミング

Atomic Designのような抽象的な分類ではなく、実際のデザインシステムの名称(Button、Card、Dialog等)でコンポーネントを設計します。ContainerやWrapperなどの技術的な名前は避けます。

// 推奨しない:技術的・抽象的な名前
components/
  ProductContainer.tsx    // ❌ Container
  UserWrapper.tsx        // ❌ Wrapper
  DataDisplay.tsx        // ❌ 汎用的すぎる

// 推奨:ビジネス・デザインの言語
components/
  ui/                   // shadcn/uiの基本コンポーネント
    button.tsx
    card.tsx
    dialog.tsx
  product/              // ビジネスドメインごとに整理
    product-card.tsx    // ✅ 製品カード
    product-list.tsx    // ✅ 製品リスト
    product-detail.tsx  // ✅ 製品詳細
  checkout/
    payment-form.tsx    // ✅ 支払いフォーム
    order-summary.tsx   // ✅ 注文サマリー

Storybookで共有したり、デザイナーやPMとの会議で同じ言語で会話できるよう、誰もが理解できる名前を使います。

パフォーマンス最適化は実測後に適用

useMemo、useCallback、React.memoは判断コストがあるため、実際にパフォーマンス問題が発生してから適用します。

useMemoは重い計算のみ

// 推奨しない:軽い処理にuseMemo
const uppercased = useMemo(() => 
  text.toUpperCase(), [text]
);

// 推奨:重い計算が実測された場合のみ
const analyzed = useMemo(() => 
  heavyDataProcessing(largeDataset), // 100ms以上かかる処理
  [largeDataset]
);

useCallbackは依存配列が安定している場合のみ

// 推奨しない:すべての関数にuseCallback
const handleClick = useCallback(() => {
  console.log('clicked');
}, []);

// 推奨:子コンポーネントの再レンダリング防止が必要な場合
const handleFilter = useCallback((term: string) => {
  setFilteredData(data.filter(item => item.name.includes(term)));
}, [data]); // 依存配列が頻繁に変わらない場合

React.memoは複雑なコンポーネントのみ

React.memoはpropsの差分チェックを行い、変更がない場合は再レンダリングをスキップします。リスト内の個別アイテムや複雑なテーブルで効果的です。

// 推奨しない:単純なコンポーネント
const SimpleText = React.memo(({ text }) => 
  <span>{text}</span>
);

// 推奨:複雑なテーブルの行コンポーネント
const TableRow = React.memo(({ rowData, columns }) => {
  return (
    <tr>
      {columns.map(col => (
        <td key={col.id}>
          {renderCell(rowData, col)}
        </td>
      ))}
    </tr>
  );
});

// 1000行のテーブルで各行の再レンダリングを防ぐ
function DataTable({ rows, columns }) {
  return (
    <table>
      <tbody>
        {rows.map(row => (
          <TableRow 
            key={row.id}
            rowData={row}
            columns={columns}
          />
        ))}
      </tbody>
    </table>
  );
}

React DevToolsのProfilerで実測し、レンダリング時間が長い箇所のみ最適化を適用します。

構造を明確化する

Props Drillingを避け、childrenを活用したコンポジションで、アプリケーションの構造を視覚的に理解しやすくします。

// 推奨しない:Props Drilling で構造が見えにくい
function App({ user, settings, notifications }) {
  return <Layout user={user} settings={settings} notifications={notifications} />;
}

// 推奨:コンポジションで構造を可視化
function App({ user, settings, notifications }) {
  return (
    <Layout>
      <Header>
        <Logo />
        <Nav />
        <UserMenu user={user} notifications={notifications} />
      </Header>
      <MainContent>
        <Dashboard settings={settings} />
      </MainContent>
    </Layout>
  );
}

// LayoutやHeaderはchildrenを受け取るだけ
function Layout({ children }) {
  return <div className="min-h-screen flex flex-col">{children}</div>;
}

function Header({ children }) {
  return (
    <header className="border-b">
      <div className="container flex items-center justify-between h-16">
        {children}
      </div>
    </header>
  );
}

コンポーネントツリーがJSXの構造に直接反映され、どこに何があるか一目で理解できます。中間コンポーネントは単純にchildrenを受け渡すだけで、データの流れが明確になります。

よくある問題

shadcn/uiのコンポーネントが要件に合わない

基本はshadcn/uiのコンポーネントを拡張して対応します。完全に新規作成が必要な場合は、同じAPIスタイルを維持します。

コンポーネントの粒度に迷う

デザインシステムの名称を基準にします。ButtonやCardなど、デザイナーと共有できる名前を使います。

パフォーマンスの計測方法がわからない

React DevToolsのProfilerタブで実際の描画時間を計測します。数値的な根拠なしに最適化は行いません。

REST APIとGraphQLの混在

段階的な移行や外部APIの利用により、TanStack QueryとGraphQLが混在することがあります。弊社では両方の共存を認めています。

// TanStack QueryでREST API(外部サービスなど)
import { useQuery } from '@tanstack/react-query';

function useWeatherData(city: string) {
  return useQuery({
    queryKey: ['weather', city],
    queryFn: () => fetch(`/api/weather/${city}`).then(res => res.json()),
  });
}

// gql.tadaでGraphQL(自社API)
import { graphql } from '@/graphql';

const ProductFragment = graphql(`
  fragment ProductData on Product {
    id
    name
    price
  }
`);

// 同じコンポーネントで両方使用可能
export function ProductWithWeather({ product, city }) {
  const productData = useFragment(ProductFragment, product);
  const { data: weather } = useWeatherData(city);
  
  return (
    <Card>
      <CardContent>
        <h3>{productData.name}</h3>
        <p>価格: ¥{productData.price}</p>
        {weather && <p>現在の気温: {weather.temp}</p>}
      </CardContent>
    </Card>
  );
}

外部APIはTanStack Query、自社GraphQL APIはgql.tadaという使い分けが一般的です。

コンポーネントの命名に迷う

ContainerやWrapperなどの技術的な名前は避け、ProductCard、PaymentForm、OrderSummaryなど、ビジネスやUIの観点から理解できる名前を使います。Storybookで共有する際も同じ名前を使います。