component-architecture について
React、Vue、Svelte向けに、UIコンポーネントの設計、実装、レビューを支援するスキルです。既存コードの調査から、構成や状態管理、パフォーマンス、アクセシビリティ、検証まで扱います。
主な機能
- 01既存のコンポーネント構造とパターンの把握
- 02構成方法と状態管理の選択支援
- 03コンポーネントの整理と実装
- 04描画パフォーマンスとアクセシビリティへの対応
- 05コンポーネントの検証手順
活用シーン
- 01再利用可能なUIコンポーネントの構築
- 02コンポーネント階層のリファクタリング
- 03状態の範囲と構成パターンの選択
- 04パフォーマンスとアクセシビリティのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill component-architectureインストール後、Claude Code で/component-architectureを入力して読み込まれたか確認します。
同梱ファイルreferences/component-patterns.mdscripts/validate-components.sh
詳細
リソース
scripts/
validate-components.sh
references/
component-patterns.md
コンポーネントアーキテクチャ
このスキルでは、GoodVibes の高精度な分析ツールを使って UI コンポーネントを設計、実装する方法を説明します。適切なコンポジション、状態管理、パフォーマンス最適化を行いながら、React、Vue、Svelte のコンポーネントを構築するときに、このワークフローを使用してください。
このスキルを使用するタイミング
次の場合に、このスキルを読み込んでください。
- 新しい UI コンポーネントやコンポーネントライブラリを構築するとき
- コンポーネント階層やコンポジションパターンをリファクタリングするとき
- コンポーネントのレンダリングパフォーマンスを最適化するとき
- コンポーネントのファイル構成を整理するとき
- デザインシステムやアトミックデザインパターンを実装するとき
- コンポーネントフレームワーク間を移行するとき
- 保守性を考慮してコンポーネントアーキテクチャをレビューするとき
トリガーフレーズ:「コンポーネントを構築」「UI を作成」「コンポーネント構造」「レンダリングの最適化」「状態のリフトアップ」「コンポーネントのコンポジション」。
基本的なワークフロー
フェーズ 1:調査
コンポーネントを構築する前に、コードベースの既存パターンを把握します。
ステップ 1.1:コンポーネント構造を把握する
discover を使用して、すべてのコンポーネントファイルを見つけ、構成パターンを把握します。
discover:
queries:
- id: react_components
type: glob
patterns: ["**/*.tsx", "**/*.jsx"]
- id: vue_components
type: glob
patterns: ["**/*.vue"]
- id: svelte_components
type: glob
patterns: ["**/*.svelte"]
verbosity: files_only
ここから分かること:
- 使用中のフレームワーク(React、Vue、Svelte、または混在)
- コンポーネントファイルの構成(機能別、アトミック、フラット)
- 命名規則(PascalCase、kebab-case)
- ファイルの同居パターン(コンポーネントとテスト、スタイル)
ステップ 1.2:コンポーネントパターンを分析する
discover を使用して、コンポジションパターン、状態管理、スタイリング手法を見つけます。
discover:
queries:
- id: composition_patterns
type: grep
pattern: "(children|render|slot|as\\s*=)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: state_management
type: grep
pattern: "(useState|useReducer|reactive|writable|createSignal)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
- id: styling_approach
type: grep
pattern: "(className|styled|css|tw`|@apply)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: performance_hooks
type: grep
pattern: "(useMemo|useCallback|memo|computed|\\$:)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
verbosity: files_only
ここから分かること:
- コンポジション戦略(children props、render props、スロット)
- 状態管理パターン(フック、ストア、シグナル)
- スタイリング手法(CSS modules、Tailwind、styled-components)
- 使用中のパフォーマンス最適化手法
ステップ 1.3:コンポーネントのシンボルを抽出する
シンボル抽出機能を使って precision_read を実行し、コンポーネントのエクスポートを把握します。
precision_read:
files:
- path: "src/components/Button/index.tsx" # Example component
extract: symbols
symbol_filter: ["function", "class", "interface", "type"]
verbosity: standard
ここから分かること:
- コンポーネントのエクスポートパターン(名前付きかデフォルトか)
- Props インターフェースの定義
- ヘルパー関数とフック
- 型定義とジェネリクス
ステップ 1.4:代表的なコンポーネントを読む
構造の整ったコンポーネントを 2〜3 個読み、実装パターンを把握します。
precision_read:
files:
- path: "src/components/Button/Button.tsx"
extract: content
- path: "src/components/Form/Form.tsx"
extract: content
output:
max_per_item: 100
verbosity: standard
フェーズ 2:意思決定
ステップ 2.1:コンポーネントの構成パターンを選択する
構成を決める際は、references/component-patterns.md の判断ツリーを参照します。
一般的なパターン:
- アトミックデザイン - atoms、molecules、organisms、templates、pages
- 機能別 - 機能やドメインごとにグループ化
- フラット - すべてのコンポーネントを単一のディレクトリに配置
- ハイブリッド - 共有コンポーネントと機能別コンポーネント
判断要因:
- チームの規模(大規模なチームほど、より構造化する)
- アプリケーションの複雑さ(複雑な場合は機能別)
- デザインシステムの有無(ある場合はアトミックデザイン)
- コンポーネントの再利用性(高い場合は shared/ui ディレクトリ)
ステップ 2.2:コンポジションパターンを選択する
詳しい比較は references/component-patterns.md を参照してください。
パターンの選択ガイド:
| パターン | 使用する場面 | フレームワークのサポート |
|---|---|---|
| Children props | シンプルなラッパーコンポーネント | React、Vue(スロット)、Svelte |
| Render props | 動的なレンダリングロジック | React(レガシー) |
| Compound components | 関連コンポーネント間で状態を共有する場合 | React、Vue、Svelte |
| Higher-Order Components | 横断的関心事 | React(レガシー) |
| Hooks/Composables | ロジックの再利用 | React、Vue 3、Svelte |
| Slots | テンプレートベースのコンポジション | Vue、Svelte |
現代的な推奨事項:
- React:Hooks + children props(HOC/render props は避ける)
- Vue:Composition API + スロット
- Svelte:Actions + スロット
ステップ 2.3:状態管理戦略を選択する
コンポーネントレベルの状態:
- UI のみの関心事(モーダル、フォーム、トグル)にはローカル状態を使用する
- 兄弟コンポーネント間で共有する必要がある場合にのみ、状態をリフトアップする
- 状態を重複させず、導出する
アプリケーションレベルの状態:
- React:Context + useReducer、Zustand、Jotai
- Vue:Pinia(Vue 3)、Vuex(Vue 2)
- Svelte:ストア、Context API
状態管理の判断ツリーは references/component-patterns.md を参照してください。
フェーズ 3:実装
ステップ 3.1:コンポーネントインターフェースを定義する
まず、props の TypeScript インターフェースを定義します。
React の例:
import { ReactNode } from 'react';
interface ButtonProps {
/** Button content */
children: ReactNode;
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Size preset */
size?: 'sm' | 'md' | 'lg';
/** Loading state */
isLoading?: boolean;
/** Disabled state */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
/** Additional CSS classes */
className?: string;
}
ベストプラクティス:
- すべての props に JSDoc コメントで説明を記述する
- バリアント props には判別可能なユニオン型を使用する
?を使ってオプションの props を明示する- 適切なデフォルト値を設定する
any型は避ける
Vue 3 の例:
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
variant: {
type: String as PropType<'primary' | 'secondary' | 'ghost' | 'danger'>,
default: 'primary',
},
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
default: 'md',
},
isLoading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
});
Svelte の例:
// Button.svelte (Svelte 4)
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' | 'danger' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let isLoading = false;
export let disabled = false;
</script>
// Button.svelte (Svelte 5 - using $props rune)
<script lang="ts">
let { variant = 'primary', size = 'md', isLoading = false, disabled = false }: {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
disabled?: boolean;
} = $props();
</script>
ステップ 3.2:コンポーネントのロジックを実装する
コンポーネントの実装には、フレームワーク固有のパターンに従います。
コンポジションを使用した React:
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Spinner } from './Spinner';
const buttonVariants = {
variant: {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
ghost: 'hover:bg-gray-100 text-gray-700',
danger: 'bg-red-600 hover:bg-red-700 text-white',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
className,
...props
},
ref
) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus-visible:outline-none focus-visible:ring-2',
'disabled:pointer-events-none disabled:opacity-50',
buttonVariants.variant[variant],
buttonVariants.size[size],
className
)}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<>
<Spinner className="mr-2 h-4 w-4" aria-hidden />
<span>Loading...</span>
</>
) : (
children
)}
</button>
);
}
);
Button.displayName = 'Button';
主なパターン:
- DOM ref を公開するために
forwardRefを使用する - 柔軟性を高めるために
...propsを展開する - ユーティリティ関数で classNames を組み合わせる
- ローディング状態や無効状態を処理する
- アクセシビリティのために ARIA 属性を追加する
ステップ 3.3:コンポーネントファイルを整理する
適切なファイル構成でコンポーネントディレクトリを作成します。
標準的な構成:
components/
Button/
Button.tsx # Component implementation
Button.test.tsx # Unit tests
Button.stories.tsx # Storybook stories (if using)
index.tsx # Barrel export
types.ts # Type definitions (if complex)
精密ツールを使った実装:
precision_write:
files:
- path: "src/components/Button/Button.tsx"
content: |
import { forwardRef } from 'react';
// ... [full implementation]
- path: "src/components/Button/index.tsx"
content: |
export { Button } from './Button';
export type { ButtonProps } from './Button';
- path: "src/components/Button/Button.test.tsx"
content: |
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
// ... [test cases]
verbosity: count_only
フェーズ 4:状態管理
ステップ 4.1:状態のスコープを特定する
ローカル状態(コンポーネント内のみ):
- フォーム入力
- モーダルの開閉
- ドロップダウンの展開・折りたたみ
- ローディング状態
親に持ち上げた状態(親が管理):
- 複数のフィールドにまたがるフォーム検証
- マルチステップウィザードの状態
- 一度に 1 つだけ開くアコーディオンの状態
グローバル状態(アプリ全体):
- ユーザー認証
- テーマ設定
- ショッピングカート
- 通知
ステップ 4.2:状態パターンを実装する
React - ローカル状態:
import { useState } from 'react';
interface SearchResult {
id: string;
title: string;
description: string;
}
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const handleSearch = async () => {
const data = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
setResults(await data.json());
};
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</>
);
}
React - 親に持ち上げた状態:
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
return (
<>
<SearchInput query={query} onQueryChange={setQuery} />
<SearchResults results={results} />
</>
);
}
React - 派生状態:
interface Item {
id: string;
category: string;
name: string;
}
interface FilteredListProps {
items: Item[];
filter: string;
}
function FilteredList({ items, filter }: FilteredListProps) {
// Don't store filtered items in state - derive them
const filteredItems = items.filter(item => item.category === filter);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
ステップ 4.3:プロップドリリングを避ける
深くネストされた props には Context を使用する:
import { createContext, useContext } from 'react';
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
フェーズ 5:パフォーマンスの最適化
ステップ 5.1:レンダリングの問題を特定する
分析ツールを使ってパフォーマンスの問題を検出します。
mcp__plugin_goodvibes_frontend-engine__trace_component_state:
component_file: "src/components/Dashboard.tsx"
target_component: "Dashboard"
mcp__plugin_goodvibes_frontend-engine__analyze_render_triggers:
component_file: "src/components/ExpensiveList.tsx"
これらのツールでわかること:
- 不必要に再レンダリングされるコンポーネント
- 状態の更新によって連鎖的に発生するレンダリング
- 親コンポーネントのレンダリングごとに変化する props
ステップ 5.2:メモ化を適用する
コストの高い計算をメモ化する:
import { useMemo } from 'react';
interface DataItem {
id: string;
[key: string]: unknown;
}
interface DataTableProps {
data: DataItem[];
filters: Record<string, any>;
}
function DataTable({ data, filters }: DataTableProps) {
const filteredData = useMemo(() => {
return data.filter(item => matchesFilters(item, filters));
}, [data, filters]);
return <table>{/* render filteredData */}</table>;
}
コールバック関数をメモ化する:
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Prevent Child re-render when count changes
const handleClick = useCallback(() => {
onClick();
}, []);
return <Child onClick={handleClick} />;
}
コンポーネントをメモ化する:
import { memo } from 'react';
interface ExpensiveChildProps {
data: DataItem[];
}
const ExpensiveChild = memo(function ExpensiveChild({ data }: ExpensiveChildProps) {
// Only re-renders if data changes
return <div>{/* complex rendering */}</div>;
});
ステップ 5.3:仮想化を実装する
大きなリストには、仮想化ライブラリを使用します。
import { useVirtualizer } from '@tanstack/react-virtual';
interface VirtualListProps<T = unknown> {
items: T[];
}
function VirtualList({ items }: VirtualListProps) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
// Note: Inline styles acceptable here for virtualization positioning
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
ステップ 5.4:コンポーネントを遅延読み込みする
React の遅延読み込み:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
フェーズ 6:アクセシビリティ
ステップ 6.1:セマンティック HTML
div の代わりに、適切な HTML 要素を使用します。
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
ステップ 6.2:ARIA 属性
スクリーンリーダー向けに ARIA ラベルを追加します。
interface DialogProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}
function Dialog({ isOpen, onClose, children }: DialogProps) {
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<div id="dialog-description">{children}</div>
<button onClick={onClose} aria-label="Close dialog">
X
</button>
</div>
);
}
ステップ 6.3:キーボード操作
すべてのインタラクティブ要素をキーボードで操作できるようにします。
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'Escape') setIsOpen(false);
if (e.key === 'Enter' || e.key === ' ') setIsOpen(!isOpen);
};
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
aria-haspopup="true"
>
Menu
</button>
{isOpen && <div role="menu">{/* menu items */}</div>}
</div>
);
}
ステップ 6.4:アクセシビリティを検証する
フロントエンド分析ツールを使ってアクセシビリティを確認します。
mcp__plugin_goodvibes_frontend-engine__get_accessibility_tree:
component_file: "src/components/Dialog.tsx"
フェーズ 7:検証
ステップ 7.1:型チェック
TypeScript のコンパイルを確認します。
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
ステップ 7.2:コンポーネント検証スクリプトを実行する
検証スクリプトを使って品質を確保します。
bash plugins/goodvibes/skills/outcome/component-architecture/scripts/validate-components.sh .
検証スイートの全容については、scripts/validate-components.sh を参照してください。
ステップ 7.3:ビジュアルリグレッションテスト
Storybook などを使用している場合は、ビジュアルテストを実行します。
precision_exec:
commands:
- cmd: "npm run test:visual"
expect:
exit_code: 0
verbosity: minimal
よくあるアンチパターン
避けること:
- 派生状態を保存する(既存の状態から計算する)
- props や状態を直接変更する
- 動的なリストの key にインデックスを使う
- レンダリング時に新しいオブジェクトや関数を作成する
- props の検証や TypeScript の型を省略する
- コンポーネントの props に
any型を使う - 表示ロジックとビジネスロジックを混在させる
- アクセシビリティ(ARIA、キーボード操作)を無視する
- 過剰に最適化する(時期尚早なメモ化)
推奨すること:
- 可能な場合は props から状態を導出する
- props と状態を不変として扱う
- リスト項目には安定した一意の key を使う
- 関数をレンダリングの外で定義するか、
useCallbackを使う - TypeScript で厳密な props の型を定義する
- コンテナ(ロジック)と表示コンポーネントを分離する
- カスタムコンポーネントに ARIA 属性を追加する
- 最適化の前に計測する(React DevTools Profiler)
クイックリファレンス
発見フェーズ:
discover: { queries: [components, patterns, state, styling], verbosity: files_only }
precision_read: { files: [example components], extract: symbols }
実装フェーズ:
precision_write: { files: [Component.tsx, index.tsx, types.ts], verbosity: count_only }
パフォーマンス分析:
trace_component_state: { component_file: "src/...", target_component: "Name" }
analyze_render_triggers: { component_file: "src/..." }
アクセシビリティチェック:
get_accessibility_tree: { component_file: "src/..." }
検証フェーズ:
precision_exec: { commands: [{ cmd: "npm run typecheck" }] }
実装後:
bash scripts/validate-components.sh .
詳細なパターン、フレームワークの比較、意思決定ツリーについては、references/component-patterns.md を参照してください。
関連スキル
以下の GoodVibes の補完的なスキルの利用をご検討ください。
- styling-system - デザインシステムのトークン、テーマアーキテクチャ、CSS-in-JS のパターン
- state-management - グローバル状態のパターン、ストアアーキテクチャ、データフロー
- testing-strategy - コンポーネントテスト、ビジュアルリグレッション、アクセシビリティテスト
- performance-audit - バンドル分析、レンダリングのプロファイリング、最適化戦略
- accessibility-audit - WCAG 準拠、スクリーンリーダーテスト、ARIA のパターン