mcpField
C
component-refactoring

component-refactoring

github.com/langflow-ai開発156.3k60 回インストール1 日前に更新
リポジトリ

component-refactoring について

Langflowのフロントエンドにある複雑なReactコンポーネントをリファクタリングするための開発スキルです。複雑度の評価、分割パターン、段階的な検証手順を開発者に提供します。

主な機能

  1. 01コンポーネント複雑度の手動評価
  2. 02カスタムフックとサブコンポーネントの抽出
  3. 03条件分岐とフォーム処理の簡素化
  4. 04Langflowのディレクトリと状態規約への準拠
  5. 05変更を段階的に検証

活用シーン

  1. 01300行を超えるコンポーネントの分割
  2. 02複雑な状態と副作用の整理
  3. 03深くネストした条件分岐のリファクタリング

インストール

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

$npx skills add https://github.com/langflow-ai/langflow --skill component-refactoring

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

同梱ファイル

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

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

詳細

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

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

複雑度のしきい値:複雑度が 50 を超えるコンポーネント(条件分岐、ネストの深さ、行数を手作業で数えて測定)は、テスト前にリファクタリングしてください。

クイックリファレンス

コマンド(src/frontend/ から実行)
cd src/frontend

# Lint with Biome
npm run lint

# Type checking
npm run type-check

# Run tests
npm test
複雑度の手動評価

Langflow には自動複雑度分析ツールがないため、コンポーネントを手動で評価します。

  1. 条件分岐を数える:各 if/else、switch/case、三項演算子、&&/|| の連鎖に +1 を加えます。
  2. ネストの深さを数える:条件分岐またはループ内のネストが 1 段深くなるごとに +1 を加えます。
  3. 総行数を数える:コンポーネントファイルあたり 300 行未満を目標にします。
  4. ステートフックを数える:useState の呼び出しが 5 回を超える場合は、フックの抽出を検討してください。
  5. エフェクトを数える:useEffect の呼び出しが 3 回を超える場合は、エフェクトの統合を検討してください。
複雑度スコアの解釈
スコアレベル対応
0-25シンプルテスト可能
26-50中程度小規模なリファクタリングを検討
51-75複雑テスト前にリファクタリング
76-100非常に複雑必ずリファクタリング

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

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

タイミング:コンポーネントに複雑なステート管理、複数の useState/useEffect、または UI と混在したビジネスロジックがある場合。

Langflow の規約:フックは hooks/ サブディレクトリに配置するか、コンポーネントと同じ場所に use-<feature>.ts として配置します。Langflow では use- プレフィックスを付けたケバブケースのファイル名を使用します。

// Before: Complex state logic in component
const FlowPage: FC = () => {
  const [nodes, setNodes] = useState<Node[]>([])
  const [edges, setEdges] = useState<Edge[]>([])
  const [buildStatus, setBuildStatus] = useState<BuildStatus>(BuildStatus.IDLE)

  // 50+ lines of state management logic...

  return <div>...</div>
}

// After: Extract to custom hook
// hooks/use-flow-state.ts
export const useFlowState = (flowId: string) => {
  const [nodes, setNodes] = useState<Node[]>([])
  const [edges, setEdges] = useState<Edge[]>([])
  const [buildStatus, setBuildStatus] = useState<BuildStatus>(BuildStatus.IDLE)

  // Related state management logic here

  return { nodes, setNodes, edges, setEdges, buildStatus, setBuildStatus }
}

// Component becomes cleaner
const FlowPage: FC = () => {
  const { nodes, setNodes, edges, setEdges } = useFlowState(flowId)
  return <div>...</div>
}

Langflow の例:

  • src/frontend/src/hooks/use-add-component.ts
  • src/frontend/src/hooks/use-unsaved-changes.ts
  • src/frontend/src/hooks/use-refresh-model-inputs.ts
パターン 2:サブコンポーネントを抽出する

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

Langflow の規約:サブコンポーネントはサブディレクトリ、または同じディレクトリ内の別ファイルに配置します。UI プリミティブは components/ui/、ドメインコンポーネントは components/core/、再利用可能なコンポーネントは components/common/ に配置します。

// Before: Monolithic JSX with multiple sections
const GenericNode = () => {
  return (
    <div>
      {/* 100 lines of header UI */}
      {/* 100 lines of parameter fields */}
      {/* 100 lines of output handles */}
    </div>
  )
}

// After: Split into focused components
// CustomNodes/GenericNode/
//   generic-node.tsx       (orchestration only — kebab-case, descriptive name)
//   components/
//     node-header.tsx
//     node-parameters.tsx
//     node-outputs.tsx

const GenericNode = () => {
  return (
    <div>
      <NodeHeader nodeData={data} />
      <NodeParameters fields={fields} />
      <NodeOutputs outputs={outputs} />
    </div>
  )
}

Langflow の例:

  • src/frontend/src/CustomNodes/GenericNode/components/
  • src/frontend/src/components/core/
  • src/frontend/src/components/ui/
パターン 3:条件分岐を簡素化する

タイミング:ネストが深い(3 段を超える)、三項演算子が複雑、または if/else の連鎖が複数ある場合。

// Before: Deeply nested conditionals
const getFieldComponent = (field: InputFieldType) => {
  if (field.type === "str") {
    if (field.multiline) {
      return <TextAreaComponent />
    } else if (field.password) {
      return <PasswordInput />
    } else if (field.options?.length) {
      return <Dropdown options={field.options} />
    } else {
      return <InputComponent />
    }
  } else if (field.type === "int") {
    return <IntComponent />
  } else if (field.type === "float") {
    return <FloatComponent />
  }
  return null
}

// After: Use lookup tables + early returns
const FIELD_COMPONENT_MAP: Record<string, FC<FieldProps>> = {
  int: IntComponent,
  float: FloatComponent,
  bool: ToggleComponent,
  code: CodeAreaComponent,
}

const STR_VARIANT_MAP: Record<string, FC<FieldProps>> = {
  multiline: TextAreaComponent,
  password: PasswordInput,
  options: Dropdown,
}

const getFieldComponent = (field: InputFieldType) => {
  if (field.type !== "str") {
    const Component = FIELD_COMPONENT_MAP[field.type]
    return Component ? <Component {...field} /> : null
  }

  const variant = field.multiline ? "multiline"
    : field.password ? "password"
    : field.options?.length ? "options"
    : "default"

  const Component = STR_VARIANT_MAP[variant] ?? InputComponent
  return <Component {...field} />
}
パターン 4:API/データロジックを抽出する

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

Langflow の規約:

  • このスキルはコンポーネントの分解を対象としており、クエリ/ミューテーションの設計を対象としていません。
  • データ取得をリファクタリングする際は、クエリパターン、UseRequestProcessor、キャッシュの無効化、ミューテーションのエラー処理について frontend-query-mutation を使用します。
  • リファクタリング中に、処理をそのまま通すだけの薄い useQuery ラッパーを作成しないでください。複数のクエリ/ミューテーションや共有する派生ステートを実際に調整する場合に限り、カスタムフックを抽出します。
  • API フックは controllers/API/queries/{domain}/ に配置します。

Langflow の例:

  • src/frontend/src/controllers/API/queries/flows/use-post-add-flow.ts
  • src/frontend/src/controllers/API/queries/variables/use-get-global-variables.ts
  • src/frontend/src/controllers/API/queries/folders/use-get-folders.ts
パターン 5:モーダル/ダイアログの管理を抽出する

タイミング:コンポーネントが、複雑な開閉状態を持つ複数のモーダルを管理している場合。

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

// Before: Multiple modal states in component
const FlowToolbar = () => {
  const [showExportModal, setShowExportModal] = useState(false)
  const [showShareModal, setShowShareModal] = useState(false)
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
  const [showApiModal, setShowApiModal] = useState(false)
  // 5+ more modal states...
}

// After: Extract to modal management hook
type ModalType = "export" | "share" | "delete" | "api" | null

const useFlowToolbarModals = () => {
  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:フォームロジックを抽出する

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

Langflow の規約:フォームの状態とバリデーションはフックに抽出します。

// Extract form validation and submission
const useFlowSettingsForm = (initialValues: FlowSettings) => {
  const [values, setValues] = useState(initialValues)
  const [errors, setErrors] = useState<Record<string, string>>({})

  const validate = useCallback(() => {
    const newErrors: Record<string, string> = {}
    if (!values.name?.trim()) newErrors.name = "Name is required"
    if (values.endpoint_name && !/^[a-z0-9-]+$/.test(values.endpoint_name)) {
      newErrors.endpoint_name = "Must be lowercase alphanumeric with hyphens"
    }
    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }, [values])

  const handleChange = useCallback((field: string, value: any) => {
    setValues((prev) => ({ ...prev, [field]: value }))
  }, [])

  return { values, errors, validate, handleChange }
}

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

1. Zustand ストアセレクター

対象:コンポーネントが Zustand ストアから多くの値を読み取り、不要な再レンダリングが発生している場合。

// Before: Selecting too many values
const Component = () => {
  const flowStore = useFlowStore()
  // Component re-renders on ANY store change
}

// After: Use individual selectors
const Component = () => {
  const nodes = useFlowStore((state) => state.nodes)
  const edges = useFlowStore((state) => state.edges)
  // Component only re-renders when nodes or edges change
}

Langflow の参考例:src/frontend/src/stores/ 内のすべてのストアで、このセレクターパターンに従っています。

2. カスタムノードコンポーネント

対象:フローノードコンポーネント(CustomNodes/GenericNode/)をリファクタリングする場合。

規約:

  • ノードのロジックはカスタムフックにまとめます
  • パラメーターのレンダリングは別のコンポーネントに切り出します
  • サブコンポーネントには既存の components/ サブディレクトリを使用します
CustomNodes/GenericNode/
  generic-node.tsx             # Node registration and main render (kebab-case, NOT index.tsx)
  components/
    handle-render.tsx           # Handle rendering
    node-description.tsx        # Node description display
    node-input-field.tsx        # Input field rendering
    node-name.tsx               # Node name display
    node-output-field.tsx       # Output field rendering
    node-status.tsx             # Build status display
3. フローキャンバスコンポーネント

対象:フローエディターのキャンバスに関連するコンポーネントをリファクタリングする場合。

規約:

  • キャンバスライブラリには @xyflow/react v12 を使用します
  • キャンバスのイベントハンドラーと UI のレンダリングを分離します
  • フローの状態管理には useFlowStore を使用します
  • 複数のフローの管理には useFlowsManagerStore を使用します
4. API クエリフックを使用するコンポーネント

対象:API データを使用するコンポーネントをリファクタリングする場合。

規約:

  • controllers/API/queries/ にある既存のクエリフックを使用します
  • 新しいクエリ/ミューテーションには UseRequestProcessor を使用します
  • キャッシュ管理には ["useGetGlobalVariables"] のようなクエリキーの配列を使用します
  • キャッシュの無効化はミューテーションの onSettled コールバック内で行います

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

ステップ 1:複雑さを評価する

以下を手動で数えます:

  • 条件分岐の総数(if/else、switch、三項演算子、&&/||)
  • ネストの最大深度
  • コードの総行数
  • useState/useEffect フックの数
  • 個別の UI セクションの数
ステップ 2:計画する

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

検出した特徴リファクタリングの対応
関連する状態を扱う useState フックが 5 つ以上カスタムフックに切り出す
コンポーネント本体内の API 呼び出しクエリフックに切り出す
ロジックを含むイベントハンドラーが 3 つ以上イベントハンドラーをフックに切り出す
300 行以上サブコンポーネントに分割する
条件分岐のネストが深い(3 段階超)条件分岐のロジックを簡潔にする
モーダルの状態が複数あるモーダル管理を切り出す
ステップ 3:段階的に実行する
  1. 一度に 1 つずつ切り出します
  2. 切り出すたびに lint、型チェック、テストを実行します
  3. 次のステップに進む前に機能を確認します
For each extraction:
  1. Extract code
  2. Run: npm run lint
  3. Run: npm run type-check
  4. Run: npm test
  5. Test functionality manually
  6. PASS? -> Next extraction
     FAIL? -> Fix before continuing
ステップ 4:確認する

リファクタリング後、複雑さを手動で再評価します。

  • 複雑さの目標値:50 未満
  • 行数の目標値:300 未満
  • ネストの最大深度の目標値:3 以下
  • 関数の最大行数の目標値: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 }
}
既存のパターンを崩す
  • components/ui/、components/core/、components/common/ の既存のディレクトリ構成に従います
  • 命名規則を維持します(ファイル名は kebab-case、コンポーネント名は PascalCase)
  • 互換性を保つため、エクスポートのパターンを維持します
  • Zustand ストアセレクターのパターンに一貫性を持たせます
早すぎる抽象化
  • 複雑さを明確に軽減できる場合にのみ切り出します
  • 一度しか使わないコードのために抽象化を作成しないでください
  • リファクタリングしたコードは、同じドメイン領域内に保ちます
UseRequestProcessor を迂回する
  • API 呼び出しに useQuery や useMutation を直接使用しないでください
  • リトライと無効化のロジックに一貫性を持たせるため、必ず UseRequestProcessor パターンを使用します
  • API フックのパターンについては、frontend-query-mutation スキルを参照してください

参考資料

Langflow のコードベースの例
  • フックの切り出し:src/frontend/src/hooks/
  • コンポーネントの分割:src/frontend/src/CustomNodes/GenericNode/components/
  • UI コンポーネント:src/frontend/src/components/ui/
  • コアコンポーネント:src/frontend/src/components/core/
  • 共通コンポーネント:src/frontend/src/components/common/
  • API クエリフック:src/frontend/src/controllers/API/queries/
  • Zustand ストア:src/frontend/src/stores/
関連スキル
  • frontend-query-mutation - API クエリおよびミューテーションのパターンについて
  • frontend-testing - リファクタリングしたコンポーネントのテストについて

コメント