mcpField
C
component-refactoring

component-refactoring

github.com/charlie85270開発35770 回インストール2 週間前に更新
リポジトリ

component-refactoring について

Dify フロントエンドの複雑な React コンポーネントをリファクタリングするスキルです。複雑度分析、Dify 固有の分割パターン、段階的な検証手順を組み合わせています。

主な機能

  1. 01コンポーネントの複雑度と指標を分析
  2. 02リファクタリング用プロンプトを生成
  3. 03フックとサブコンポーネントの分割指針
  4. 04Dify 固有のリファクタリング規約
  5. 05段階的な lint、型チェック、テスト手順

活用シーン

  1. 01複雑な Dify React コンポーネントの分割
  2. 02フックや UI セクションの抽出判断
  3. 03指標とテストによるリファクタリングの検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charlie85270/dorothy --skill component-refactoring

インストール後、Claude Code で/component-refactoringを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
complexity-patterns.md
component-splitting.md
hook-extraction.md

詳細

Dify コンポーネントのリファクタリングスキル

以下のパターンとワークフローに従って、Dify のフロントエンドコードベースにある複雑度の高い React コンポーネントをリファクタリングします。

複雑度のしきい値:複雑度が 50 を超えるコンポーネント(pnpm analyze-component で測定)は、テストの前にリファクタリングしてください。

クイックリファレンス

コマンド(web/ から実行)

web/ からの相対パスを使用してください(例:app/components/...)。 リファクタリングのプロンプトには refactor-component を、テストのプロンプトとメトリクスには analyze-component を使用してください。

cd web

# Generate refactoring prompt
pnpm refactor-component <path>

# Output refactoring analysis as JSON
pnpm refactor-component <path> --json

# Generate testing prompt (after refactoring)
pnpm analyze-component <path>

# Output testing analysis as JSON
pnpm analyze-component <path> --json
複雑度の分析
# Analyze component complexity
pnpm analyze-component <path> --json

# Key metrics to check:
# - complexity: normalized score 0-100 (target < 50)
# - maxComplexity: highest single function complexity
# - lineCount: total lines (target < 300)
複雑度スコアの解釈
スコアレベルアクション
0-25🟢 シンプルテストの準備完了
26-50🟡 中程度軽微なリファクタリングを検討
51-75🟠 複雑テスト前にリファクタリング
76-100🔴 非常に複雑リファクタリング必須

主なリファクタリングパターン

パターン 1:カスタムフックを抽出する

対象:コンポーネントの状態管理が複雑な場合、複数の useState/useEffect がある場合、またはビジネスロジックと UI が混在している場合です。

Dify の規約:フックは hooks/ サブディレクトリに配置するか、コンポーネントと同じ階層に use-<feature>.ts として配置します。

// ❌ Before: Complex state logic in component
const Configuration: FC = () => {
  const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
  const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>(...)
  const [completionParams, setCompletionParams] = useState<FormValue>({})
  
  // 50+ lines of state management logic...
  
  return <div>...</div>
}

// ✅ After: Extract to custom hook
// hooks/use-model-config.ts
export const useModelConfig = (appId: string) => {
  const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
  const [completionParams, setCompletionParams] = useState<FormValue>({})
  
  // Related state management logic here
  
  return { modelConfig, setModelConfig, completionParams, setCompletionParams }
}

// Component becomes cleaner
const Configuration: FC = () => {
  const { modelConfig, setModelConfig } = useModelConfig(appId)
  return <div>...</div>
}

Dify の例:

  • web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts
  • web/app/components/app/configuration/debug/hooks.tsx
  • web/app/components/workflow/hooks/use-workflow.ts
パターン 2:サブコンポーネントを抽出する

対象:1 つのコンポーネントに複数の UI セクション、条件付きレンダリングブロック、または繰り返しのパターンがある場合です。

Dify の規約:サブコンポーネントはサブディレクトリに配置するか、同じディレクトリ内の別ファイルに配置します。

// ❌ Before: Monolithic JSX with multiple sections
const AppInfo = () => {
  return (
    <div>
      {/* 100 lines of header UI */}
      {/* 100 lines of operations UI */}
      {/* 100 lines of modals */}
    </div>
  )
}

// ✅ After: Split into focused components
// app-info/
//   ├── index.tsx           (orchestration only)
//   ├── app-header.tsx      (header UI)
//   ├── app-operations.tsx  (operations UI)
//   └── app-modals.tsx      (modal management)

const AppInfo = () => {
  const { showModal, setShowModal } = useAppInfoModals()
  
  return (
    <div>
      <AppHeader appDetail={appDetail} />
      <AppOperations onAction={handleAction} />
      <AppModals show={showModal} onClose={() => setShowModal(null)} />
    </div>
  )
}

Dify の例:

  • web/app/components/app/configuration/ ディレクトリ構造
  • ノードごとに整理された web/app/components/workflow/nodes/
パターン 3:条件分岐ロジックを簡素化する

対象:深いネスト(3 階層超)、複雑な三項演算子、または複数の if/else チェーンがある場合です。

// ❌ Before: Deeply nested conditionals
const Template = useMemo(() => {
  if (appDetail?.mode === AppModeEnum.CHAT) {
    switch (locale) {
      case LanguagesSupported[1]:
        return <TemplateChatZh />
      case LanguagesSupported[7]:
        return <TemplateChatJa />
      default:
        return <TemplateChatEn />
    }
  }
  if (appDetail?.mode === AppModeEnum.ADVANCED_CHAT) {
    // Another 15 lines...
  }
  // More conditions...
}, [appDetail, locale])

// ✅ After: Use lookup tables + early returns
const TEMPLATE_MAP = {
  [AppModeEnum.CHAT]: {
    [LanguagesSupported[1]]: TemplateChatZh,
    [LanguagesSupported[7]]: TemplateChatJa,
    default: TemplateChatEn,
  },
  [AppModeEnum.ADVANCED_CHAT]: {
    [LanguagesSupported[1]]: TemplateAdvancedChatZh,
    // ...
  },
}

const Template = useMemo(() => {
  const modeTemplates = TEMPLATE_MAP[appDetail?.mode]
  if (!modeTemplates) return null
  
  const TemplateComponent = modeTemplates[locale] || modeTemplates.default
  return <TemplateComponent appDetail={appDetail} />
}, [appDetail, locale])
パターン 4:API/データロジックを抽出する

対象:コンポーネントが API 呼び出し、データ変換、または複雑な非同期処理を直接扱っている場合です。

Dify の規約:web/service/use-*.ts にある @tanstack/react-query のフックを使用するか、カスタムデータフックを作成します。

// ❌ Before: API logic in component
const MCPServiceCard = () => {
  const [basicAppConfig, setBasicAppConfig] = useState({})
  
  useEffect(() => {
    if (isBasicApp && appId) {
      (async () => {
        const res = await fetchAppDetail({ url: '/apps', id: appId })
        setBasicAppConfig(res?.model_config || {})
      })()
    }
  }, [appId, isBasicApp])
  
  // More API-related logic...
}

// ✅ After: Extract to data hook using React Query
// use-app-config.ts
import { useQuery } from '@tanstack/react-query'
import { get } from '@/service/base'

const NAME_SPACE = 'appConfig'

export const useAppConfig = (appId: string, isBasicApp: boolean) => {
  return useQuery({
    enabled: isBasicApp && !!appId,
    queryKey: [NAME_SPACE, 'detail', appId],
    queryFn: () => get<AppDetailResponse>(`/apps/${appId}`),
    select: data => data?.model_config || {},
  })
}

// Component becomes cleaner
const MCPServiceCard = () => {
  const { data: config, isLoading } = useAppConfig(appId, isBasicApp)
  // UI only
}

Dify における React Query のベストプラクティス:

  • クエリーキーを整理するために NAME_SPACE を定義します
  • 条件付きフェッチには enabled オプションを使用します
  • データ変換には select を使用します
  • 無効化フックをエクスポートします:useInvalidXxx

Dify の例:

  • web/service/use-workflow.ts
  • web/service/use-common.ts
  • web/service/knowledge/use-dataset.ts
  • web/service/knowledge/use-document.ts
パターン 5:モーダル/ダイアログ管理を抽出する

対象:コンポーネントが複雑な開閉状態を持つ複数のモーダルを管理している場合です。

Dify の規約:モーダルは状態管理とともに抽出します。

// ❌ Before: Multiple modal states in component
const AppInfo = () => {
  const [showEditModal, setShowEditModal] = useState(false)
  const [showDuplicateModal, setShowDuplicateModal] = useState(false)
  const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  const [showSwitchModal, setShowSwitchModal] = useState(false)
  const [showImportDSLModal, setShowImportDSLModal] = useState(false)
  // 5+ more modal states...
}

// ✅ After: Extract to modal management hook
type ModalType = 'edit' | 'duplicate' | 'delete' | 'switch' | 'import' | null

const useAppInfoModals = () => {
  const [activeModal, setActiveModal] = useState<ModalType>(null)
  
  const openModal = useCallback((type: ModalType) => setActiveModal(type), [])
  const closeModal = useCallback(() => setActiveModal(null), [])
  
  return {
    activeModal,
    openModal,
    closeModal,
    isOpen: (type: ModalType) => activeModal === type,
  }
}
パターン 6:フォームロジックを抽出する

対象:フォームのバリデーション、送信処理、またはフィールドの変換が複雑な場合です。

Dify の規約:web/app/components/base/form/ にある @tanstack/react-form のパターンを使用します。

// ✅ Use existing form infrastructure
import { useAppForm } from '@/app/components/base/form'

const ConfigForm = () => {
  const form = useAppForm({
    defaultValues: { name: '', description: '' },
    onSubmit: handleSubmit,
  })
  
  return <form.Provider>...</form.Provider>
}

Dify 固有のリファクタリングガイドライン

1. コンテキストプロバイダーの抽出

対象:コンポーネントが複数の状態を含む複雑なコンテキスト値を提供している場合です。

// ❌ Before: Large context value object
const value = {
  appId, isAPIKeySet, isTrailFinished, mode, modelModeType,
  promptMode, isAdvancedMode, isAgent, isOpenAI, isFunctionCall,
  // 50+ more properties...
}
return <ConfigContext.Provider value={value}>...</ConfigContext.Provider>

// ✅ After: Split into domain-specific contexts
<ModelConfigProvider value={modelConfigValue}>
  <DatasetConfigProvider value={datasetConfigValue}>
    <UIConfigProvider value={uiConfigValue}>
      {children}
    </UIConfigProvider>
  </DatasetConfigProvider>
</ModelConfigProvider>

Dify の参照先:web/context/ ディレクトリ構造

2. ワークフローノードコンポーネント

対象:ワークフローノードのコンポーネント(web/app/components/workflow/nodes/)をリファクタリングする場合です。

規約:

  • ノードのロジックは use-interactions.ts にまとめます
  • パネル UI は別ファイルに抽出します
  • 共通パターンには _base コンポーネントを使用します
nodes/<node-type>/
  ├── index.tsx              # Node registration
  ├── node.tsx               # Node visual component
  ├── panel.tsx              # Configuration panel
  ├── use-interactions.ts    # Node-specific hooks
  └── types.ts               # Type definitions
3. 設定コンポーネント

対象:アプリ設定コンポーネントをリファクタリングする場合です。

規約:

  • 設定セクションをサブディレクトリに分けます
  • web/app/components/app/configuration/ にある既存のパターンを使用します
  • 機能の切り替えは専用コンポーネントにまとめます
4. ツール/プラグインコンポーネント

対象:ツール関連のコンポーネント(web/app/components/tools/)をリファクタリングする場合です。

規約:

  • 既存のモーダルパターンに従います
  • web/service/use-tools.ts のサービスフックを使用します
  • プロバイダー固有のロジックは分離します

リファクタリングのワークフロー

ステップ 1:リファクタリング用プロンプトを生成する
pnpm refactor-component <path>

このコマンドでは、以下を実行します。

  • コンポーネントの複雑さと機能を分析します
  • 必要な具体的なリファクタリング作業を特定します
  • AI アシスタント用のプロンプトを生成します(macOS では自動的にクリップボードにコピーされます)
  • 検出されたパターンに基づいて、詳細な要件を提示します
ステップ 2:詳細を分析する
pnpm analyze-component <path> --json

以下を確認します。

  • 複雑さの合計スコア
  • 関数の最大複雑度
  • 行数
  • 検出された機能(状態、エフェクト、API など)
ステップ 3:計画する

検出された機能に基づいて、リファクタリング計画を作成します。

検出された機能リファクタリング作業
hasState: true + hasEffects: trueカスタムフックを抽出する
hasAPI: trueデータ/サービスフックを抽出する
hasEvents: true (many)イベントハンドラーを抽出する
lineCount > 300サブコンポーネントに分割する
maxComplexity > 50条件分岐のロジックを簡素化する
ステップ 4:段階的に実行する
  1. 一度に 1 つずつ抽出します
  2. 抽出するたびに lint、型チェック、テストを実行します
  3. 次のステップに進む前に、機能を確認します
For each extraction:
  ┌────────────────────────────────────────┐
  │ 1. Extract code                        │
  │ 2. Run: pnpm lint:fix                  │
  │ 3. Run: pnpm type-check:tsgo           │
  │ 4. Run: pnpm test                      │
  │ 5. Test functionality manually         │
  │ 6. PASS? → Next extraction             │
  │    FAIL? → Fix before continuing       │
  └────────────────────────────────────────┘
ステップ 5:確認する

リファクタリングの完了後:

# Re-run refactor command to verify improvements
pnpm refactor-component <path>

# If complexity < 25 and lines < 200, you'll see:
# ✅ COMPONENT IS WELL-STRUCTURED

# For detailed metrics:
pnpm analyze-component <path> --json

# Target metrics:
# - complexity < 50
# - lineCount < 300
# - maxComplexity < 30

避けるべきよくある間違い

❌ 過剰な設計
// ❌ Too many tiny hooks
const useButtonText = () => useState('Click')
const useButtonDisabled = () => useState(false)
const useButtonLoading = () => useState(false)

// ✅ Cohesive hook with related state
const useButtonState = () => {
  const [text, setText] = useState('Click')
  const [disabled, setDisabled] = useState(false)
  const [loading, setLoading] = useState(false)
  return { text, setText, disabled, setDisabled, loading, setLoading }
}
❌ 既存のパターンを壊す
  • 既存のディレクトリ構造に従います
  • 命名規則を維持します
  • 互換性のためにエクスポートのパターンを維持します
❌ 早すぎる抽象化
  • 複雑さを明確に軽減できる場合にのみ抽出します
  • 1 回しか使わないコードのために抽象化を作成しないでください
  • リファクタリングしたコードは同じドメイン領域に配置します

参考資料

Dify のコードベースの例
  • フックの抽出:web/app/components/app/configuration/hooks/
  • コンポーネントの分割:web/app/components/app/configuration/
  • サービスフック:web/service/use-*.ts
  • ワークフローのパターン:web/app/components/workflow/hooks/
  • フォームのパターン:web/app/components/base/form/
関連スキル
  • frontend-testing - リファクタリングしたコンポーネントのテスト
  • web/testing/testing.md - テスト仕様

コメント