AgentSkillsCN

component-extraction

指导用户从大型文件中提取可复用的 UI 组件,以提升代码可维护性。适用于用户希望“提取组件”、“拆分组件”、“分解大文件”、“创建可复用组件”,或在讨论组件组织与架构时使用。

SKILL.md
--- frontmatter
name: component-extraction
description: Guide for extracting reusable UI components from large files to improve maintainability. Use when user wants to "extract component", "split component", "break up large file", "create reusable component", or when discussing component organization and architecture.
metadata:
  author: Claude
  version: 1.0.0
  category: architecture
  tags: [components, refactoring, ui, svelte, react]

Component Extraction Skill

Purpose

Expert guidance on when and how to extract components from large files, with focus on creating maintainable, reusable UI pieces.

Decision Framework: When to Extract

The 400/600 Rule

code
< 400 lines + single use     → Keep as-is
400-600 lines + single use   → Extract sections with comments
600+ lines OR 2+ uses        → Extract components
1000+ lines                  → MUST extract

The Reuse Test

Ask: "Will this exact UI be used elsewhere?"

  • Yes → Extract immediately
  • Maybe → Wait for 2nd use
  • No, but file is 600+ lines → Extract anyway for maintainability

The Responsibility Test

Does this section handle:

  1. Its own data fetching?
  2. Complex state management?
  3. Multiple unrelated UI concerns?

If yes to 2+ → Extract component

Extraction Patterns

Pattern 1: Filter Panel Extraction

Before (in +page.svelte):

svelte
<script lang="ts">
  let searchQuery = $state('');
  let startDate = $state('2024-01-01');
  let endDate = $state('2024-12-31');
  let category = $state('all');
  
  // 150 lines of filter UI
</script>

<div class="filters">
  <input bind:value={searchQuery} placeholder="Search..." />
  <input type="date" bind:value={startDate} />
  <input type="date" bind:value={endDate} />
  <select bind:value={category}>
    <option value="all">All</option>
    <!-- ... more options -->
  </select>
  <!-- ... more filter controls -->
</div>

After (extracted component):

svelte
<!-- components/ExpenseFilters.svelte -->
<script lang="ts">
  interface Props {
    search: string;
    dateRange: { start: string; end: string };
    category: string;
    categories: string[];
    onsearch: (query: string) => void;
    ondatechange: (range: { start: string; end: string }) => void;
    oncategorychange: (category: string) => void;
  }
  
  const { 
    search,
    dateRange,
    category,
    categories,
    onsearch,
    ondatechange,
    oncategorychange
  }: Props = $props();
  
  let localSearch = $state(search);
  let localStart = $state(dateRange.start);
  let localEnd = $state(dateRange.end);
  
  // Debounce search
  $effect(() => {
    const timeout = setTimeout(() => {
      if (localSearch !== search) onsearch(localSearch);
    }, 300);
    return () => clearTimeout(timeout);
  });
</script>

<div class="filters">
  <input bind:value={localSearch} placeholder="Search..." />
  <input 
    type="date" 
    value={localStart}
    onchange={e => {
      localStart = e.currentTarget.value;
      ondatechange({ start: localStart, end: localEnd });
    }}
  />
  <input 
    type="date" 
    value={localEnd}
    onchange={e => {
      localEnd = e.currentTarget.value;
      ondatechange({ start: localStart, end: localEnd });
    }}
  />
  <select 
    value={category}
    onchange={e => oncategorychange(e.currentTarget.value)}
  >
    <option value="all">All</option>
    {#each categories as cat}
      <option value={cat}>{cat}</option>
    {/each}
  </select>
</div>

<style>
  .filters {
    /* component-specific styles */
  }
</style>

Usage in page (now ~10 lines):

svelte
<script lang="ts">
  import ExpenseFilters from './components/ExpenseFilters.svelte';
  
  let search = $state('');
  let dateRange = $state({ start: '2024-01-01', end: '2024-12-31' });
  let category = $state('all');
</script>

<ExpenseFilters
  {search}
  {dateRange}
  {category}
  categories={$userSettings.expenseCategories}
  onsearch={q => search = q}
  ondatechange={range => dateRange = range}
  oncategorychange={c => category = c}
/>

Pattern 2: Data Table Extraction

Before (in +page.svelte):

svelte
<!-- 400 lines of table rendering -->
<table>
  <thead>
    <tr>
      <th>
        <input type="checkbox" 
          checked={selectedAll} 
          onchange={toggleSelectAll}
        />
      </th>
      <th>Date</th>
      <th>Amount</th>
      <!-- ... more headers -->
    </tr>
  </thead>
  <tbody>
    {#each paginatedItems as item}
      <tr>
        <td>
          <input type="checkbox" 
            checked={selectedIds.has(item.id)}
            onchange={() => toggleSelect(item.id)}
          />
        </td>
        <td>{formatDate(item.date)}</td>
        <td>{formatCurrency(item.amount)}</td>
        <!-- ... more cells -->
      </tr>
    {/each}
  </tbody>
</table>

After (extracted generic table):

svelte
<!-- lib/components/data-table/DataTable.svelte -->
<script lang="ts" generics="T extends { id: string }">
  import type { Column } from './types';
  
  interface Props {
    items: T[];
    columns: Column<T>[];
    selectable?: boolean;
    selectedIds?: Set<string>;
    onselect?: (id: string) => void;
    onselectall?: () => void;
    onedit?: (item: T) => void;
    ondelete?: (item: T) => void;
  }
  
  const {
    items,
    columns,
    selectable = false,
    selectedIds = new Set(),
    onselect,
    onselectall,
    onedit,
    ondelete
  }: Props = $props();
  
  let allSelected = $derived(
    items.length > 0 && items.every(item => selectedIds.has(item.id))
  );
</script>

<table>
  <thead>
    <tr>
      {#if selectable}
        <th class="select-col">
          <input 
            type="checkbox" 
            checked={allSelected}
            onchange={onselectall}
          />
        </th>
      {/if}
      {#each columns as col}
        <th>{col.header}</th>
      {/each}
      {#if onedit || ondelete}
        <th>Actions</th>
      {/if}
    </tr>
  </thead>
  <tbody>
    {#each items as item (item.id)}
      <tr>
        {#if selectable}
          <td>
            <input 
              type="checkbox"
              checked={selectedIds.has(item.id)}
              onchange={() => onselect?.(item.id)}
            />
          </td>
        {/if}
        {#each columns as col}
          <td>
            {#if col.render}
              {@render col.render(item)}
            {:else if col.format}
              {col.format(item[col.key])}
            {:else}
              {item[col.key]}
            {/if}
          </td>
        {/each}
        {#if onedit || ondelete}
          <td class="actions">
            {#if onedit}
              <button onclick={() => onedit(item)}>Edit</button>
            {/if}
            {#if ondelete}
              <button onclick={() => ondelete(item)}>Delete</button>
            {/if}
          </td>
        {/if}
      </tr>
    {/each}
  </tbody>
</table>

Usage (now ~30 lines):

svelte
<script lang="ts">
  import DataTable from '$lib/components/data-table/DataTable.svelte';
  import type { Column } from '$lib/components/data-table/types';
  import type { Expense } from '$lib/types';
  
  const columns: Column<Expense>[] = [
    { key: 'date', header: 'Date', format: formatDate },
    { key: 'category', header: 'Category' },
    { key: 'amount', header: 'Amount', format: formatCurrency },
    { 
      key: 'description', 
      header: 'Description',
      render: (expense) => (
        {#snippet}
          <span class="truncate">{expense.description}</span>
        {/snippet}
      )
    }
  ];
</script>

<DataTable
  items={filteredExpenses}
  {columns}
  selectable
  selectedIds={selection.selectedIds}
  onselect={selection.toggleSelection}
  onselectall={selection.toggleAll}
  onedit={handleEdit}
  ondelete={handleDelete}
/>

Pattern 3: Form Section Extraction

Before (2000-line form):

svelte
<script lang="ts">
  // 100 lines of state
  let tripName = $state('');
  let tripDate = $state('');
  let stops = $state<Stop[]>([]);
  let fuelCost = $state(0);
  let maintenance = $state<MaintenanceCost[]>([]);
  // ... 50+ more fields
</script>

<form>
  <!-- Basic info section: 200 lines -->
  <div class="section">
    <input bind:value={tripName} />
    <input type="date" bind:value={tripDate} />
    <!-- ... -->
  </div>
  
  <!-- Stops section: 400 lines -->
  <div class="section">
    {#each stops as stop, i}
      <!-- Complex stop editor -->
    {/each}
  </div>
  
  <!-- Cost section: 300 lines -->
  <div class="section">
    <!-- Cost inputs -->
  </div>
  
  <!-- ... more sections -->
</form>

After (extracted sections):

svelte
<!-- components/TripBasicInfo.svelte -->
<script lang="ts">
  interface Props {
    name: string;
    date: string;
    onupdate: (data: { name: string; date: string }) => void;
  }
  
  const { name, date, onupdate }: Props = $props();
  
  let localName = $state(name);
  let localDate = $state(date);
  
  $effect(() => {
    onupdate({ name: localName, date: localDate });
  });
</script>

<div class="basic-info">
  <label>
    Trip Name
    <input bind:value={localName} required />
  </label>
  <label>
    Date
    <input type="date" bind:value={localDate} required />
  </label>
</div>

<!-- components/TripStopsEditor.svelte -->
<script lang="ts">
  import type { Stop } from '$lib/types';
  
  interface Props {
    stops: Stop[];
    onupdate: (stops: Stop[]) => void;
  }
  
  const { stops, onupdate }: Props = $props();
  
  let localStops = $state([...stops]);
  
  function addStop() {
    localStops = [...localStops, { address: '', notes: '' }];
    onupdate(localStops);
  }
  
  function removeStop(index: number) {
    localStops = localStops.filter((_, i) => i !== index);
    onupdate(localStops);
  }
</script>

<div class="stops-editor">
  <h3>Stops</h3>
  {#each localStops as stop, i}
    <div class="stop">
      <input bind:value={stop.address} placeholder="Address" />
      <input bind:value={stop.notes} placeholder="Notes" />
      <button onclick={() => removeStop(i)}>Remove</button>
    </div>
  {/each}
  <button onclick={addStop}>Add Stop</button>
</div>

<!-- Main form page (now ~200 lines) -->
<script lang="ts">
  import TripBasicInfo from './components/TripBasicInfo.svelte';
  import TripStopsEditor from './components/TripStopsEditor.svelte';
  import TripCostInputs from './components/TripCostInputs.svelte';
  
  let tripData = $state({
    name: '',
    date: '',
    stops: [],
    costs: { fuel: 0, maintenance: [] }
  });
  
  async function handleSubmit() {
    await createTrip(tripData);
  }
</script>

<form onsubmit={handleSubmit}>
  <TripBasicInfo
    name={tripData.name}
    date={tripData.date}
    onupdate={data => tripData = { ...tripData, ...data }}
  />
  
  <TripStopsEditor
    stops={tripData.stops}
    onupdate={stops => tripData = { ...tripData, stops }}
  />
  
  <TripCostInputs
    costs={tripData.costs}
    onupdate={costs => tripData = { ...tripData, costs }}
  />
  
  <button type="submit">Create Trip</button>
</form>

Component Communication Patterns

Props Down, Events Up

svelte
<!-- Parent -->
<script lang="ts">
  let value = $state('');
</script>

<MyComponent 
  {value}
  onchange={newValue => value = newValue}
/>

<!-- Child -->
<script lang="ts">
  interface Props {
    value: string;
    onchange: (value: string) => void;
  }
  
  const { value, onchange }: Props = $props();
</script>

<input 
  {value}
  oninput={e => onchange(e.currentTarget.value)}
/>

Shared State (Stores)

typescript
// lib/stores/formState.svelte.ts
export class FormState {
  data = $state({});
  errors = $state({});
  
  update(key: string, value: unknown) {
    this.data = { ...this.data, [key]: value };
  }
  
  setError(key: string, error: string) {
    this.errors = { ...this.errors, [key]: error };
  }
}

// Usage in components
import { formState } from '$lib/stores/formState.svelte';

// Component A
formState.update('name', 'Alice');

// Component B reads the same state
const name = formState.data.name;

Context API

svelte
<!-- Parent.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  
  const formContext = {
    register: (field: string) => { /* ... */ },
    unregister: (field: string) => { /* ... */ }
  };
  
  setContext('form', formContext);
</script>

<slot />

<!-- Child.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  
  const form = getContext('form');
  form.register('email');
</script>

Anti-Patterns

❌ Over-Extraction

svelte
<!-- DON'T: Extract every 5-line section -->
<TinyButton />      <!-- 5 lines -->
<TinyLabel />       <!-- 3 lines -->
<TinyInput />       <!-- 8 lines -->
<TinyCheckbox />    <!-- 6 lines -->

<!-- DO: Keep simple UI together -->
<div class="form-group">
  <label>Email</label>
  <input type="email" />
  <span class="help-text">We'll never share your email</span>
</div>

❌ Prop Drilling

svelte
<!-- DON'T: Pass props through 5 levels -->
<A prop1={x} prop2={y} prop3={z}>
  <B prop1={x} prop2={y}>
    <C prop1={x}>
      <D prop1={x} />
    </C>
  </B>
</A>

<!-- DO: Use context or stores -->
<script lang="ts">
  setContext('config', { x, y, z });
</script>
<A>
  <B>
    <C>
      <D />
    </C>
  </B>
</A>

❌ God Components

svelte
<!-- DON'T: One component that does everything -->
<SuperDataTableWithFiltersAndPaginationAndSortingAndExport
  config={massiveConfigObject}
/>

<!-- DO: Compose smaller pieces -->
<DataTable items={filtered}>
  <FilterBar slot="filters" />
  <Pagination slot="footer" />
</DataTable>

Extraction Checklist

Before extracting:

  • Component is 200+ lines OR used 2+ times
  • Has clear, single responsibility
  • Can define clean props interface
  • Won't need frequent parent access
  • Won't cause excessive prop drilling

After extracting:

  • Component has descriptive name
  • Props are well-typed
  • Events are documented
  • Works independently (can be tested in isolation)
  • Parent code is simpler

Quick Decision Guide

Question: Should I extract this?

code
Is it used in 2+ places?
└─ YES → Extract now
└─ NO → Continue...

Is parent file > 600 lines?
└─ YES → Extract
└─ NO → Continue...

Is this section > 200 lines?
└─ YES → Extract
└─ NO → Keep together

Getting Help

Ask:

  • "Should I extract this component?"
  • "How do I pass data between these components?"
  • "Is this component doing too much?"
  • "What's the best way to structure this form?"