Dlaczego Svelte przyciąga uwagę: kontekst i porównanie z innymi frameworkami
Co to jest Svelte i czego celowo nie robi
Svelte to framework (a dokładniej: kompilator) do budowy interfejsów webowych, który przenosi większość pracy z przeglądarki do etapu budowania aplikacji. Zamiast dużej biblioteki runtime’u dostajesz skompilowany, zwykły JavaScript, HTML i CSS.
Svelte nie próbuje być wszystkim naraz. W odróżnieniu od Angulara nie dostajesz z pudełka rozbudowanego routera, warstwy HTTP, systemu modułów i DI. Svelte to przede wszystkim:
- system komponentów z reaktywnością kompilowaną do czystego JS,
- mechanizm sklepów do stanu współdzielonego,
- prosty model zdarzeń i slotów,
- integracja z bundlerami (Vite, Rollup, Webpack) przez wtyczki.
Do routingu, architektury aplikacji, wzorców projektowych używasz zewnętrznych bibliotek lub SvelteKit (meta-frameworka dla aplikacji wielostronicowych i SSR). Ten minimalizm jest jednocześnie zaletą i ograniczeniem – więcej swobody, ale też więcej decyzji po stronie zespołu.
Kompilator zamiast virtual DOM – inna filozofia niż w React
W React i wielu innych frameworkach rdzeniem jest virtual DOM. Komponent zwraca drzewo elementów (np. JSX), a biblioteka w runtime’ie porównuje stare i nowe drzewo, by zaktualizować prawdziwy DOM jak najefektywniej.
Svelte podchodzi do tematu inaczej. Zamiast interpretować drzewo w przeglądarce, kompilator analizuje komponent podczas budowania i generuje kod JS, który:
- tworzy konkretne węzły DOM (bez generowania pośredniego drzewa),
- wie dokładnie, które fragmenty trzeba zaktualizować przy zmianie danych,
- nie potrzebuje dużej warstwy runtime’u, bo logika aktualizacji jest „wypalona” w kodzie.
Efekt: mniej abstrakcji w przeglądarce, mniej kodu do pobrania i często wyraźnie szybsze pierwsze renderowanie, zwłaszcza przy mniejszych projektach i widgetach osadzanych na stronach.
Różnice odczuwalne dla frontend developera
Największa różnica na co dzień to brak klas komponentów i brak hooków. Komponent w Svelte to jeden plik z trzema sekcjami: <script>, HTML i <style>. Logika to zwykły JavaScript, a reaktywność jest związana z przypisaniami do zmiennych.
Zamiast zawiłych hooków i zależności w tablicach efektów masz prosty mechanizm reaktywnych deklaracji $:. Zamiast klas z metodami componentDidMount masz blok onMount lub zwykły kod uruchamiany po stworzeniu komponentu.
Dla programisty przekłada się to na mniejszą ilość „ceremonii”. Prosty komponent to często kilkanaście linijek bez boilerplate’u. Mniej się walczy z frameworkiem, więcej pisze samej logiki i widoków.
Kiedy Svelte ma najwięcej sensu
Svelte szczególnie dobrze działa w sytuacjach, gdzie liczy się lekkość, szybkość startu i prostota kodu:
- małe i średnie aplikacje SPA lub MPA,
- panele administracyjne, narzędzia wewnętrzne, dashboardy,
- osadzane widgety (np. formularze na stronach, okienka czatu, małe aplikacje wbudowane w istniejący serwis),
- szybkie prototypowanie nowych produktów, gdzie istotny jest krótki time-to-market.
Jeśli zespół jest niewielki i chcesz szybko zbudować coś działającego, Svelte daje dużo w krótkim czasie. Szczególnie, gdy nie potrzebujesz od razu całej infrastruktury w stylu dużego monolitycznego frameworka.
Kiedy React, Angular czy Vue nadal wygrywają
W dużych, wieloletnich projektach przewagą Reacta czy Angulara jest ekosystem, dostępność programistów i narzędzi, oraz masa wypracowanych dobrych praktyk. Kilka kontekstów, gdzie duże frameworki mogą być bezpieczniejszym wyborem:
- organizacja ma już duży ekosystem komponentów w React/Angular i nie planuje migracji,
- potrzebne są zaawansowane rozwiązania enterprise: rozbudowane formularze, walidacje, internacjonalizacja, i wszystko ma być „z pudełka”,
- projekt wymaga wielu gotowych bibliotek UI (design systemy dostarczane od dostawcy SaaS), które wspierają tylko React.
Svelte rozwija się szybko, ale wciąż ma mniejszy ekosystem niż React. W niektórych niszowych obszarach (np. bardzo specyficzne komponenty wizualne) możesz po prostu nie znaleźć gotowej biblioteki i trzeba będzie pisać własne rozwiązania.
Jak działa Svelte pod spodem: kompilator, bundler i runtime
Od pliku .svelte do czystego JavaScriptu
Plik .svelte zawiera trzy główne części: sekcję skryptu, znacznik HTML oraz sekcję stylów. Kompilator przetwarza go i generuje moduł JS z funkcjami tworzącymi i aktualizującymi konkretny komponent.
Schemat przepływu jest prosty:
- piszesz komponent
MyComponent.svelte, - Vite/Rollup przez wtyczkę uruchamia kompilator Svelte,
- powstaje plik JS z funkcją tworzącą DOM, funkcją aktualizacji i funkcją niszczenia komponentu,
- dołączone zostają małe utilsy runtime’owe (np. do patchowania DOM, planowania aktualizacji).
Dla przeglądarki to po prostu kolejny moduł JS, bez znajomości pojęcia „Svelte”. Dzięki temu masz pełną kontrolę nad tym, jak aplikacja jest bundlowana i ładowana.
Rola kompilatora: optymalizacje i brak virtual DOM
Kompilator analizuje składnię komponentu, by zbudować graph zależności między zmiennymi a fragmentami DOM. Zna strukturę szablonu jeszcze przed uruchomieniem aplikacji, więc może:
- generować konkretne operacje DOM, zamiast ogólnego diffowania drzew,
- usuwać nieużywane części kodu (dead code elimination),
- łączyć style w minimalne, zasięgowe selektory,
- wykrywać proste błędy już na etapie kompilacji (np. użycie nieistniejącej zmiennej w szablonie).
Brak virtual DOM oznacza mniej narzutu w runtime’ie. Zamiast ogólnego algorytmu diffowania dostajesz szyty na miarę kod aktualizujący dokładnie te węzły, które są zależne od danej zmiennej.
Co ląduje w przeglądarce i jak wpływa to na wydajność
Do przeglądarki trafia wyłącznie rezultat kompilacji: JavaScript ze specyficznymi funkcjami tworzącymi węzły DOM i aktualizującymi ich właściwości, plus CSS posegregowany i zasięgowo powiązany z komponentami.
Mniej runtime’u oznacza zwykle:
- mniejszy rozmiar paczki JS,
- krótszy czas parsowania i wykonania kodu,
- szybsze pierwsze wyrenderowanie widoku.
Przy małych i średnim projektach różnica jest szczególnie zauważalna – nie musisz ściągać setek kilobajtów biblioteki, której używasz tylko w kilku miejscach. Dla większych projektów optymalizacja bundla nadal jest potrzebna, ale punkt wyjścia jest lżejszy.
Integracja z Vite, Rollup i Webpackiem
Typowy projekt Svelte korzysta dziś z Vite jako bundlera i dev-serwera. Vite używa wtyczki @sveltejs/vite-plugin-svelte, która:
- wykrywa pliki
.svelte, - uruchamia kompilator Svelte z odpowiednimi ustawieniami,
- obsługuje hot module replacement (HMR),
- integruje się z TypeScriptem i postprocesorami CSS.
Rollup był historycznie pierwszym „domyślnym” bundlerem dla Svelte. Wciąż jest używany w niektórych setupach, zwłaszcza tam, gdzie Vite nie jest wymogiem. Webpack działa przez wtyczkę svelte-loader, ale jest dziś rzadziej wybierany w nowych projektach.
Ograniczenia podejścia kompilatorowego
Są scenariusze, w których podejście kompilatorowe jest mniej elastyczne. Kompilator musi wiedzieć, co dzieje się w kodzie, by wygenerować optymalne operacje. Jeśli próbujesz robić bardzo dynamiczne rzeczy, np.:
- ładować komponenty na podstawie stringów z runtime’u,
- modyfikować strukturę szablonu w nietypowy, metaprogramistyczny sposób,
- tworzyć system pluginów generujących komponenty „w locie”,
kompilator może nie być w stanie tego dobrze przeanalizować. W praktyce jednak większość aplikacji mieści się w typowych scenariuszach, dla których Svelte jest optymalizowany. W bardzo zaawansowanych, silnie dynamicznych systemach warto uważnie przemyśleć architekturę i sprawdzić, czy nie narzucasz kompilatorowi zbyt wielu niewiadomych.
Pierwsze kroki z Svelte: projekt od zera krok po kroku
Minimalne wymagania i środowisko
Do startu wystarczą:
- Node.js w aktualnej LTS,
- npm, pnpm lub yarn (dowolny manager pakietów),
- prosty edytor kodu z obsługą JavaScript/TypeScript (VS Code, WebStorm, itp.).
Struktura katalogów projektu Svelte z Vite jest klasyczna: katalog główny projektu, wewnątrz src z kodem aplikacji, static (lub public) na zasoby statyczne oraz pliki konfiguracyjne bundlera.
Szybki start: create-svelte vs ręczna konfiguracja Vite
Najprostszy start zapewnia oficjalny szablon:
npm create svelte@latest my-svelte-app
cd my-svelte-app
npm install
npm run dev
Kreator zada kilka pytań (SvelteKit vs projekt SPA, TypeScript, ESLint itd.) i wygeneruje gotową strukturę. Dla prostego SPA bez SSR możesz użyć template’u Svelte + Vite:
npm create vite@latest my-svelte-spa -- --template svelte
cd my-svelte-spa
npm install
npm run dev
Ręczna konfiguracja z Vite w praktyce sprowadza się do zainstalowania wtyczki Svelte i dodania jej do vite.config.js, ale przy nowych projektach korzystanie z kreatora oszczędza czas.
Struktura typowego projektu
W projekcie Svelte z Vite najczęściej spotkasz układ:
src/main.js– punkt wejścia, w którym montujesz główny komponent,src/App.svelte– główny komponent aplikacji,src/lib/– katalog na współdzielone komponenty,src/routes/– w SvelteKit, katalog routów (pliki+page.svelteitd.),static/lubpublic/– pliki statyczne, które trafiają bezpośrednio na serwer.
Konfiguracja bundlera (Vite/Rollup) jest w osobnym pliku (vite.config.js, rollup.config.js). Tam ustawiasz m.in. aliasy do katalogów, integrację z TypeScriptem, preprocessory CSS czy obsługę środowisk.
Dev server, build produkcyjny i podgląd
Standardowy workflow z Vite wygląda następująco:
npm run dev– uruchamia dev server z HMR,npm run build– buduje wersję produkcyjną do katalogudist,npm run preview– serwuje zbudowaną aplikację lokalnie w trybie zbliżonym do produkcji.
Dev server nasłuchuje zmian w plikach .svelte i natychmiast przeładowuje komponenty, zachowując stan, gdy to możliwe. Dzięki temu szybko widzisz efekt zmian bez ręcznego odświeżania.
Pierwszy komponent z prostą logiką
Przykładowy licznik w Svelte to dosłownie kilka linii:
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Kliknięto {count} razy
</button>
Zwracają uwagę dwie rzeczy: brak this.state i brak setState. Zmienna count jest zwykłą zmienną, a przypisanie count += 1 jest sygnałem dla Svelte, by zaktualizować odpowiadający jej fragment szablonu.

Składnia komponentów Svelte: szablon, logika i style w jednym pliku
Struktura pliku .svelte i wspólna praca sekcji
Każdy komponent Svelte to pojedynczy plik, który zwykle składa się z trzech sekcji:
<script>– logika, importy, zmienne, funkcje, lifecycle,- HTML – struktura widoku, powiązania danych, pętle i warunki,
<style>– style CSS, domyślnie zakreślone do komponentu.
Przykład prostego komponentu z trzema sekcjami:
Przykład komponentu z trzema sekcjami
<script>
export let name = 'świat';
let counter = 0;
const increment = () => {
counter += 1;
};
</script>
<main>
<h1>Cześć, {name}!</h1>
<p>Kliknięcia: {counter}</p>
<button on:click={increment}>Kliknij mnie</button>
</main>
<style>
main {
padding: 1rem;
}
button {
background: #ff3e00;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
</style>
Skrypt, szablon i style żyją w jednym pliku, ale kompilator dba o ich separację. Do przeglądarki trafia już rozdzielony JS i CSS, z nałożonymi atrybutami zakresu.
Eksportowane propsy i domyślne wartości
Dane wejściowe komponentu definiujesz przez export w sekcji skryptu:
<script>
export let title = 'Domyślny tytuł';
export let items = [];
</script>
<h2>{title}</h2>
{#if items.length === 0}
<p>Brak elementów.</p>
{:else}
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
{/if}
Wywołanie w nadrzędnym komponencie wygląda naturalnie:
<List title="Zadania" {items} />
Destrukturyzacja nie jest potrzebna – każde export let staje się propertą komponentu.
Logika w <script>: moduł vs instancja
Svelte rozróżnia dwa konteksty skryptu: instancji i modułu. Standardowy blok bez atrybutów to logika instancji, uruchamiana dla każdego wystąpienia komponentu.
<script context="module">
// wspólne dla wszystkich instancji
let globalCounter = 0;
export function getGlobalCounter() {
return globalCounter;
}
export function incrementGlobalCounter() {
globalCounter += 1;
}
</script>
<script>
// osobne dla każdej instancji
let localCounter = 0;
function click() {
localCounter += 1;
incrementGlobalCounter();
}
</script>
Kontekst modułu bywa przydatny przy cache’owaniu danych lub statycznych konfiguracjach współdzielonych przez wiele instancji.
Warunki i pętle w szablonie
Zamiast JSX są bloki kontrolne oparte o składnię z krzyżykiem. Dają jasny podział logiki widoku bez mieszania się z JS-em.
{#if loading}
<p>Ładowanie...</p>
{:else if error}
<p class="error">Błąd: {error}</p>
{:else}
<ul>
{#each todos as todo, index (todo.id)}
<li class:done={todo.done}>
{index + 1}. {todo.title}
</li>
{/each}
</ul>
{/if}
Klucz w {#each} podajesz w nawiasie. Gdy props todos się zmieni, kompilator aktualizuje tylko potrzebne elementy listy.
Powiązania klas i atrybutów
Dla dynamicznych klas są krótkie skróty. Nie trzeba generować całego stringa klas ręcznie.
<button
class="btn"
class:primary={variant === 'primary'}
class:danger={variant === 'danger'}
disabled={disabled}
>
{label}
</button>
Podobnie można wiązać atrybuty i właściwości. Gdy warunek jest fałszywy, dana klasa jest usuwana z DOM.
Style zakresowe i globalne
Style w komponencie domyślnie są izolowane. Kompilator dodaje unikalny atrybut, np. data-svelte-hash, do selektorów.
<style>
h1 {
color: #ff3e00;
}
.card {
border-radius: 6px;
}
</style>
Gdy trzeba wyjść poza komponent, można użyć :global():
<style>
:global(body) {
margin: 0;
font-family: system-ui, sans-serif;
}
:global(.btn) {
cursor: pointer;
}
</style>
Przy większych projektach CSS często ląduje w osobnych plikach lub w utility frameworkach, ale lokalny CSS w komponentach zostaje wygodnym narzędziem do stylowania małych modułów.
Sloty i kompozycja komponentów
Sloty pozwalają tworzyć komponenty opakowujące, bez utraty elastyczności treści. Domyślny slot jest prosty:
<!-- Card.svelte -->
<article class="card">
<slot />
</article>
<style>
.card {
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
<Card>
<h2>Tytuł</h2>
<p>Treść karty.</p>
</Card>
Dla bardziej złożonych layoutów przydają się sloty nazwane:
<!-- Modal.svelte -->
<div class="overlay">
<div class="modal">
<header><slot name="header" /></header>
<section><slot /></section>
<footer><slot name="footer" /></footer>
</div>
</div>
<Modal>
<h2 slot="header">Usuń element</h2>
<p>Czy na pewno?</p>
<button slot="footer">Anuluj</button>
</Modal>
Taka kompozycja dobrze się sprawdza przy systemach design system / UI kit, gdzie komponenty muszą być elastyczne, ale spójne wizualnie.
Reaktywność w Svelte: inne podejście niż w React czy Vue
Reaktywność oparta na przypisaniach
Podstawą reaktywności są zwykłe przypisania. Gdy w logice komponentu zmieniasz wartość zmiennej, Svelte generuje odpowiednie aktualizacje DOM.
<script>
let count = 0;
let double;
$: double = count * 2;
function add() {
count += 1;
}
</script>
<p>Count: {count}</p>
<p>Double: {double}</p>
<button on:click={add}>+1</button>
Linia z $: jest deklaracją reaktywności. Gdy zmieni się count, kod po $: wykona się ponownie.
Reaktywne deklaracje i zależności
Reaktywne etykiety mogą zawierać zarówno proste przypisania, jak i całe bloki kodu. Kompilator śledzi, od jakich zmiennych zależy dany blok.
<script>
let a = 1;
let b = 2;
let sum;
let log;
$: sum = a + b;
$: {
log = `a=${a}, b=${b}, sum=${sum}`;
console.log(log);
}
</script>
<p>Suma: {sum}</p>
Jeśli zmieni się a lub b, przeliczy się sum, a potem wykona się blok z logiem. Nie ma tu ręcznego zarządzania zależnościami, całość rozwiązuje kompilator.
Reaktywność tablic i obiektów
Przy tablicach i obiektach trzeba zadbać o to, by wykonało się przypisanie. Modyfikacja in place bez przypisania nie wywoła aktualizacji.
<script>
let items = [1, 2, 3];
function pushItem() {
items = [...items, items.length + 1];
}
function markFirst() {
items[0] = 99;
items = items; // wymuszenie reakcji
}
</script>
W codziennej pracy i tak częściej używa się niemutujących operacji (spread, map, filter), więc wymuszanie przypisania nie jest specjalnie uciążliwe.
Asynchroniczność i reaktywne bloki
W reaktywnych blokach można korzystać z funkcji asynchronicznych, np. do ładowania danych. Typowy przykład prostego fetchowania na zmianę query:
<script>
let query = '';
let results = [];
let loading = false;
$: if (query.length > 2) {
loading = true;
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(data => {
results = data;
})
.finally(() => {
loading = false;
});
} else {
results = [];
}
</script>
Przy takim podejściu trzeba jednak zadbać o anulowanie nieaktualnych żądań lub debounce, jeśli input zmienia się często. Tu wchodzi w grę logika pomocnicza, np. w osobnym module.
Lifecycle komponentu
Hooki cyklu życia są funkcjami importowanymi ze Svelte. Używa się ich w sekcji skryptu.
<script>
import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte';
let width = 0;
onMount(() => {
width = window.innerWidth;
const handler = () => (width = window.innerWidth);
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
});
beforeUpdate(() => {
// kod przed aktualizacją DOM
});
afterUpdate(() => {
// kod po aktualizacji DOM
});
onDestroy(() => {
// sprzątanie, jeśli coś zostanie
});
</script>
Typowy scenariusz to rejestracja event listenerów, integracje z bibliotekami typu datepicker albo mapy oraz ręczne obserwatory DOM.
Dwukierunkowe wiązanie (bind)
Dwukierunkowe powiązania działają zarówno z natywnymi elementami formularzy, jak i z niektórymi właściwościami komponentu.
<script>
let name = '';
let accept = false;
</script>
<input bind:value={name} placeholder="Imię" />
<label>
<input type="checkbox" bind:checked={accept} />
Akceptuję regulamin
</label>
<p>Witaj, {name || 'nieznajomy'}</p>
<p>Zgoda: {accept ? 'tak' : 'nie'}</p>
Przy komponentach własnych można eksponować bindowalne właściwości, np. bind:value w komponencie input-wrapper. Ułatwia to spójne formularze na poziomie całej aplikacji.
Sklepy (stores) i zarządzanie stanem w Svelte
Podstawowe typy sklepów: writable, readable, derived
Sklepy są lekką abstrakcją nad stanem współdzielonym między komponentami. Rdzeń jest w pakiecie svelte/store.
<script>
import { writable, readable, derived } from 'svelte/store';
export const count = writable(0);
export const now = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});
export const formattedTime = derived(now, ($now) =>
$now.toLocaleTimeString()
);
</script>
writable ma metody set, update i subscribe, readable daje tylko odczyt, a derived przelicza wartość na podstawie innych sklepów.
Subskrypcja w komponentach: składnia $store
W komponentach można użyć skrótu z dolarem. Ułatwia to pracę z globalnym stanem bez ręcznego zarządzania subskrypcją.
<script>
import { count, formattedTime } from './stores.js';
const increment = () => count.update((n) => n + 1);
</script>
<p>Stan globalny: {$count}</p>
<p>Czas: {$formattedTime}</p>
<button on:click={increment}>+1</button>
Za kulisami kompilator generuje subscribe i dba o wypisanie się z subskrypcji przy zniszczeniu komponentu.
Własne sklepy z logiką domenową
Najpraktyczniejsze są niestandardowe sklepy z zamkniętą logiką. Można w nich ukryć reguły biznesowe i integracje z API.
// cartStore.js
import { writable } from 'svelte/store';
function createCart() {
const { subscribe, update, set } = writable([]);
return {
subscribe,
add(item) {
update((items) => {
const existing = items.find((i) => i.id === item.id);
if (existing) {
return items.map((i) =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i
);
}
return [...items, { ...item, qty: 1 }];
});
},
remove(id) {
update((items) => items.filter((i) => i.id !== id));
},
clear() {
set([]);
}
};
}
export const cart = createCart();
Komponent koszyka korzysta tylko z metod add, remove, clear i z odczytu przez $cart.
Persistencja i synchronizacja sklepów
Sklepy łatwo rozszerzyć o przechowywanie w localStorage lub synchronizację między kartami. Wystarczy owinąć writable własną funkcją.
// persistentStore.js
import { writable } from 'svelte/store';
export function persistent(key, initial) {
const stored = typeof localStorage !== 'undefined'
? localStorage.getItem(key)
: null;
const start = stored ? JSON.parse(stored) : initial;
const store = writable(start);
if (typeof window !== 'undefined') {
store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value));
});
}
return store;
}
<script>
import { persistent } from './persistentStore.js';
const theme = persistent('theme', 'light');
function toggleTheme() {
theme.update((t) => (t === 'light' ? 'dark' : 'light'));
}
</script>
<button on:click={toggleTheme}>Przełącz motyw (aktualny: {$theme})</button>
Taki sklep można potem podłączyć do klas na <body> lub zmiennych CSS.
Sklepy jako API do asynchronicznych operacji
Asynchroniczne operacje dobrze kapsułkować w sklepie. Komponenty mają wtedy prosty interfejs: stan, dane, błędy, metody akcji.
// userStore.js
import { writable } from 'svelte/store';
function createUserStore() {
const { subscribe, set, update } = writable({
user: null,
loading: false,
error: null
});
return {
subscribe,
async load() {
update((s) => ({ ...s, loading: true, error: null }));
try {
const res = await fetch('/api/me');
if (!res.ok) throw new Error('Błąd pobierania');
const user = await res.json();
set({ user, loading: false, error: null });
} catch (e) {
set({ user: null, loading: false, error: e.message });
}
},
logout() {
set({ user: null, loading: false, error: null });
// dodatkowe wylogowanie na backendzie
}
};
}
export const userStore = createUserStore();
<script>
import { onMount } from 'svelte';
import { userStore } from './userStore.js';
const { subscribe, load, logout } = userStore;
let state;
const unsubscribe = subscribe((s) => (state = s));
onMount(() => {
load();
return () => unsubscribe();
});
</script>
{#if state.loading}
<p>Ładowanie...</p>
{:else if state.error}
<p>Błąd: {state.error}</p>
{:else if state.user}
<p>Witaj, {state.user.name}</p>
<button on:click={logout}>Wyloguj</button>
{:else}
<p>Gość</p>
{/if}
Takie podejście ułatwia testowanie i wymianę źródła danych (np. REST na GraphQL).
Formularze oparte na sklepach
Przy większych formularzach wygodniej trzymać stan w sklepie zamiast w lokalnym komponencie. Komponent skupia się wtedy na HTML i błędach walidacji.
// loginFormStore.js
import { writable, derived } from 'svelte/store';
const initial = {
email: '',
password: '',
submitting: false,
error: null
};
function createLoginForm() {
const { subscribe, update, set } = writable(initial);
return {
subscribe,
setField(field, value) {
update((s) => ({ ...s, [field]: value }));
},
async submit() {
update((s) => ({ ...s, submitting: true, error: null }));
try {
// call API
await new Promise((r) => setTimeout(r, 500));
set(initial);
} catch (e) {
update((s) => ({ ...s, submitting: false, error: 'Logowanie nieudane' }));
}
},
reset() {
set(initial);
}
};
}
export const loginForm = createLoginForm();
export const isValid = derived(loginForm, ($f) =>
$f.email.includes('@') && $f.password.length >= 6
);
<script>
import { loginForm, isValid } from './loginFormStore.js';
let form;
$: form = $loginForm; // dla wygody
</script>
<form on:submit|preventDefault={() => loginForm.submit()}>
<input
type="email"
placeholder="E-mail"
bind:value={form.email}
on:input={(e) => loginForm.setField('email', e.target.value)}
/>
<input
type="password"
placeholder="Hasło"
bind:value={form.password}
on:input={(e) => loginForm.setField('password', e.target.value)}
/>
<button type="submit" disabled={!$isValid || form.submitting}>
{form.submitting ? 'Logowanie...' : 'Zaloguj'}
</button>
{#if form.error}
<p class="error">{form.error}</p>
{/if}
</form>
Sklep przejmuje odpowiedzialność za reguły formularza, komponent staje się cienką warstwą widoku.

Formularze, zdarzenia i praca z DOM w Svelte
Obsługa zdarzeń i modyfikatory
Zdarzenia obsługuje się przez on:event. Modyfikatory upraszczają typowe scenariusze jak preventDefault, stopPropagation, once, passive.
<button on:click={handleClick}>Kliknij</button>
<form on:submit|preventDefault={submitForm}>
...
</form>
<div on:click|stopPropagation={innerClick}>Wewnętrzny</div>
<button on:click|once={() => console.log('tylko raz')}>Raz</button>
Modyfikatory łączy się kropką, ale w Svelte używany jest zapis z pionową kreską.
Własne zdarzenia komponentów
Komponenty mogą emitować własne zdarzenia. Pomaga createEventDispatcher z pakietu svelte.
<!-- ConfirmButton.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let message = 'Na pewno?';
function handleClick() {
if (confirm(message)) {
dispatch('confirm');
}
}
</script>
<button on:click={handleClick}><slot>Usuń</slot></button>
<ConfirmButton on:confirm={deleteItem}>Usuń element</ConfirmButton>
Zdarzenie zachowuje się jak natywne, można na nie reagować w rodzicu i łączyć z modyfikatorami.
Dwukierunkowe powiązania z komponentami
Dwukierunkowe bindowanie można zaimplementować także w komponentach. Trzeba wystawić export let value i emulować zdarzenie input przez dispatch('input', value) lub specjalne zdarzenie.
<!-- TextInput.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let value = '';
export let label = '';
const dispatch = createEventDispatcher();
function onInput(event) {
value = event.target.value;
dispatch('change', value);
}
</script>
<label>
<span>{label}</span>
<input {value} on:input={onInput} />
</label>
<script>
import TextInput from './TextInput.svelte';
let name = '';
</script>
<TextInput bind:value={name} label="Imię" />
<p>Podano: {name}</p>
Svelte sam skojarzy bind:value z eksportowaną właściwością value i zdarzeniem o nazwie change lub input.
Walidacja formularzy i klasy błędów
Przy prostych formularzach często wystarczy HTML5 i lekkie warunki w szablonie. Bardziej złożone przypadki wymagają przechowywania błędów w stanie.
<script>
let email = '';
let errors = {};
function validate() {
const next = {};
if (!email.includes('@')) {
next.email = 'Nieprawidłowy adres e-mail';
}
errors = next;
return Object.keys(next).length === 0;
}
function submit(event) {
event.preventDefault();
if (!validate()) return;
// wysyłka danych
}
</script>
<form on:submit={submit}>
<label>
E-mail
<input
type="email"
class:error={errors.email}
bind:value={email}
/>
</label>
{#if errors.email}
<p class="error">{errors.email}</p>
{/if}
<button type="submit">Wyślij</button>
</form>
<style>
input.error {
border-color: red;
}
.error {
color: red;
font-size: 0.875rem;
}
</style>
Klauzule class:nazwa={warunek} ułatwiają dodawanie klas na podstawie stanu walidacji.
Reaktywne formularze z pętlami
Dynamiczne formularze buduje się zwykle z pętli {#each}. Przydaje się bind:value na elementach tablicy i stabilne klucze.
<script>
let items = [
{ id: 1, label: 'Imię', value: '' },
{ id: 2, label: 'Nazwisko', value: '' }
];
function addField() {
const id = Math.max(...items.map((i) => i.id)) + 1;
items = [...items, { id, label: 'Pole ' + id, value: '' }];
}
</script>
{#each items as item (item.id)}
<label>
{item.label}
<input bind:value={item.value} />
</label>
{/each}
<button type="button" on:click={addField}>Dodaj pole</button>
Dzięki (item.id) Svelte utrzymuje powiązanie pól z istniejącymi danymi, nawet przy dodawaniu i usuwaniu elementów.
Bezpośrednia praca z DOM: bind:this
Gdy trzeba sięgnąć do natywnego DOM, można związać referencję przez bind:this. Przydaje się do obsługi bibliotek zewnętrznych lub manualnych pomiarów.
<script>
import { onMount } from 'svelte';
let inputEl;
let width = 0;
onMount(() => {
inputEl.focus();
width = inputEl.getBoundingClientRect().width;
});
</script>
<input bind:this={inputEl} placeholder="Auto-focus" />
<p>Szerokość: {width}px</p>
Referencje można przekazywać też do funkcji inicjalizujących zewnętrzne widżety.
Akcje Svelte (use:)
Akcje to prosty sposób na ponowne wykorzystanie logiki DOM. Do elementu dodaje się use:nazwa, a funkcja akcji dostaje referencję elementu i opcjonalne parametry.
// actions/autoFocus.js
export function autoFocus(node) {
node.focus();
return {
destroy() {
// sprzątanie, jeśli potrzebne
}
};
}
<script>
import { autoFocus } from './actions/autoFocus.js';
</script>
<input use:autoFocus placeholder="Skupienie po montażu" />
Akcje dobrze sprawdzają się przy integracjach z tooltipami, obserwatorami przecięcia (IntersectionObserver) czy customowym drag&drop.
Akcje z parametrami i aktualizacją
Akcje przyjmują drugi argument – parametry. Mogą też zwracać metodę update, która reaguje na zmianę parametrów.
// actions/longpress.js
export function longpress(node, duration = 500) {
let timer;
const handleMousedown = () => {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
};
const clear = () => clearTimeout(timer);
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('mouseup', clear);
node.addEventListener('mouseleave', clear);
return {
update(newDuration) {
duration = newDuration;
},
destroy() {
clear();
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', clear);
node.removeEventListener('mouseleave', clear);
}
};
}
<script>
import { longpress } from './actions/longpress.js';
let time = 800;
function handleLongpress() {
alert('Długie przytrzymanie');
}
</script>
<button
use:longpress={time}
on:longpress={handleLongpress}
>
Przytrzymaj
</button>
<input type="number" bind:value={time} />
Takie małe akcje szybko budują własną bibliotekę narzędziową wykorzystywaną w całej aplikacji.
Integracja z API przeglądarki
Svelte dobrze łączy się z Web APIs, bo nie chowa DOM za grubą warstwą abstrakcji. Często wystarczą hooki cyklu życia i kilka linii kodu.
<script>
import { onMount } from 'svelte';
let position = null;
let error = null;
onMount(() => {
if (!navigator.geolocation) {
error = 'Brak wsparcia geolokalizacji';
return;
}
const watchId = navigator.geolocation.watchPosition(
(pos) => {
position = {
lat: pos.coords.latitude,
lng: pos.coords.longitude
};
},
(err) => {
error = err.message;
}
);
return () => navigator.geolocation.clearWatch(watchId);
});
</script>
{#if error}
<p>Błąd: {error}</p>
{:else if position}
<p>Lat: {position.lat}, Lng: {position.lng}</p>
{:else}
<p>Oczekiwanie na pozycję...
Co warto zapamiętać
- Svelte jest kompilatorem komponentów, który przenosi logikę z runtime’u do etapu budowania, dzięki czemu do przeglądarki trafia głównie czysty, zoptymalizowany JavaScript, HTML i CSS.
- Framework świadomie pozostaje „mały”: daje system komponentów, reaktywność, sklepy stanu i prosty model zdarzeń, a za routing, architekturę i rozbudowaną infrastrukturę odpowiadają zewnętrzne biblioteki lub SvelteKit.
- Zamiast virtual DOM Svelte generuje szyty na miarę kod operujący bezpośrednio na DOM-ie, co zmniejsza rozmiar runtime’u i poprawia szybkość pierwszego renderu, szczególnie w mniejszych aplikacjach i widgetach.
- Kod komponentu jest prosty: jeden plik z <script>, HTML i <style>, bez klas i hooków; reaktywność działa przez zwykłe przypisania i deklaracje $:, co redukuje boilerplate i ilość „magii” w porównaniu z Reactem.
- Svelte najlepiej sprawdza się w małych i średnich SPA/MPA, panelach administracyjnych, dashboardach, osadzanych widgetach oraz przy szybkim prototypowaniu, gdy liczy się lekkość bundla i krótki time-to-market.
- W dużych, wieloletnich projektach przewagę często mają React i Angular ze względu na bogaty ekosystem, gotowe rozwiązania enterprise i łatwiejszy dostęp do programistów oraz bibliotek UI.
- Kompilator Svelte analizuje zależności w komponentach, generuje konkretne operacje na DOM, usuwa martwy kod i optymalizuje style, dzięki czemu runtime jest minimalny, a przeglądarka nie „wie”, że uruchamia aplikację w Svelte.
Bibliografia
- Svelte Tutorial. Svelte – Oficjalny samouczek: składnia komponentów, reaktywność, sklepy
- Svelte API Reference. Svelte – Dokumentacja API: komponenty, cykl życia, onMount, sloty, zdarzenia
- React Documentation. Meta – Dokumentacja React: komponenty, virtual DOM, JSX, hooki, ekosystem
- Angular Documentation. Google – Dokumentacja Angular: DI, router, formularze, architektura frameworka
- Vue.js Guide. Vue.js – Oficjalny przewodnik Vue: komponenty, reaktywność, porównanie z innymi SPA






