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:
- •Its own data fetching?
- •Complex state management?
- •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?"