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.
Expert guidance on when and how to extract components from large files, with focus on creating maintainable, reusable UI pieces.
< 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
Ask: "Will this exact UI be used elsewhere?"
Does this section handle:
If yes to 2+ → Extract component
Before (in +page.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):
<!-- 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):
<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}
/>
Before (in +page.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):
<!-- 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):
<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}
/>
Before (2000-line form):
<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):
<!-- 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>
<!-- 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)}
/>
// 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;
<!-- 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>
<!-- 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>
<!-- 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>
<!-- DON'T: One component that does everything -->
<SuperDataTableWithFiltersAndPaginationAndSortingAndExport
config={massiveConfigObject}
/>
<!-- DO: Compose smaller pieces -->
<DataTable items={filtered}>
<FilterBar slot="filters" />
<Pagination slot="footer" />
</DataTable>
Before extracting:
After extracting:
Question: Should I extract this?
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
Ask: