コンポーネント設計
適切なコンポーネント設計パターンを理解することで、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で共有する際も同じ名前を使います。