mcpField
C
component-architecture

component-architecture

github.com/mgd34msu開発660 回インストール10 週間前に更新
リポジトリ

component-architecture について

React、Vue、Svelte向けに、UIコンポーネントの設計、実装、レビューを支援するスキルです。既存コードの調査から、構成や状態管理、パフォーマンス、アクセシビリティ、検証まで扱います。

主な機能

  1. 01既存のコンポーネント構造とパターンの把握
  2. 02構成方法と状態管理の選択支援
  3. 03コンポーネントの整理と実装
  4. 04描画パフォーマンスとアクセシビリティへの対応
  5. 05コンポーネントの検証手順

活用シーン

  1. 01再利用可能なUIコンポーネントの構築
  2. 02コンポーネント階層のリファクタリング
  3. 03状態の範囲と構成パターンの選択
  4. 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 のパターン

コメント