Настройка State Management (Svelte Store) для Svelte-приложения
Svelte имеет встроенную систему сторов — никаких внешних библиотек не нужно. Реактивность строится на writable, readable и derived сторах из svelte/store. Компонент подписывается на стор через префикс $ — компилятор Svelte генерирует подписку и отписку автоматически.
Для большинства Svelte-приложений встроенных сторов достаточно. Сложные случаи — cross-store derived, async-данные с состоянием загрузки, персистентность — решаются без сторонних зависимостей.
Что входит в работу
Проектирование архитектуры сторов под проект, writable/readable/derived, кастомные сторы с инкапсулированной логикой, async-данные, персистентность, типизация TypeScript, тестирование.
Базовые сторы
// stores/cart.ts
import { writable, derived, get } from 'svelte/store'
export interface CartItem {
id: string
name: string
price: number
quantity: number
}
function createCartStore() {
const { subscribe, set, update } = writable<CartItem[]>([])
return {
subscribe,
addItem(product: Omit<CartItem, 'quantity'>) {
update((items) => {
const existing = items.find((i) => i.id === product.id)
if (existing) {
return items.map((i) =>
i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
)
}
return [...items, { ...product, quantity: 1 }]
})
},
removeItem(id: string) {
update((items) => items.filter((i) => i.id !== id))
},
updateQuantity(id: string, quantity: number) {
if (quantity <= 0) {
update((items) => items.filter((i) => i.id !== id))
return
}
update((items) =>
items.map((i) => (i.id === id ? { ...i, quantity } : i))
)
},
clear() {
set([])
},
}
}
export const cart = createCartStore()
// derived — производные значения
export const cartTotal = derived(cart, ($items) =>
$items.reduce((sum, i) => sum + i.price * i.quantity, 0)
)
export const cartCount = derived(cart, ($items) =>
$items.reduce((sum, i) => sum + i.quantity, 0)
)
export const isEmpty = derived(cart, ($items) => $items.length === 0)
Использование в компоненте
<script lang="ts">
import { cart, cartTotal, cartCount, isEmpty } from '$lib/stores/cart'
function handleAddToCart(product: Product) {
cart.addItem(product)
}
</script>
<button on:click={() => handleAddToCart(product)}>
В корзину
</button>
{#if !$isEmpty}
<div class="cart-summary">
<span>Товаров: {$cartCount}</span>
<span>Итого: {$cartTotal} ₽</span>
<button on:click={() => cart.clear()}>Очистить</button>
</div>
{/if}
Префикс $ перед именем стора — авто-подписка. Компилятор Svelte трансформирует это в cart.subscribe(...) с отпиской при destroy компонента.
Auth-стор
// stores/auth.ts
import { writable, derived } from 'svelte/store'
interface AuthState {
user: User | null
token: string | null
loading: boolean
error: string | null
}
function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>({
user: null,
token: localStorage.getItem('token'),
loading: false,
error: null,
})
return {
subscribe,
async login(credentials: LoginCredentials) {
update((s) => ({ ...s, loading: true, error: null }))
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
})
if (!res.ok) throw new Error('Неверный логин или пароль')
const { user, token } = await res.json()
localStorage.setItem('token', token)
set({ user, token, loading: false, error: null })
} catch (err) {
update((s) => ({
...s,
loading: false,
error: err instanceof Error ? err.message : 'Ошибка входа',
}))
}
},
logout() {
localStorage.removeItem('token')
set({ user: null, token: null, loading: false, error: null })
},
}
}
export const auth = createAuthStore()
export const isAuthenticated = derived(auth, ($auth) => !!$auth.token)
export const currentUser = derived(auth, ($auth) => $auth.user)
Readable — только для чтения
// stores/time.ts
import { readable } from 'svelte/store'
// readable — внешний источник данных
export const currentTime = readable<Date>(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000)
return () => clearInterval(interval) // cleanup
})
// WebSocket-стор
export const livePrice = readable<number | null>(null, (set) => {
const ws = new WebSocket('wss://api.example.com/price')
ws.onmessage = (e) => set(JSON.parse(e.data).price)
ws.onerror = () => set(null)
return () => ws.close()
})
Async-данные с состоянием
// stores/products.ts
import { writable, derived } from 'svelte/store'
interface AsyncState<T> {
data: T | null
loading: boolean
error: string | null
}
function createAsyncStore<T>() {
const { subscribe, set, update } = writable<AsyncState<T>>({
data: null,
loading: false,
error: null,
})
return {
subscribe,
async load(fetcher: () => Promise<T>) {
update((s) => ({ ...s, loading: true, error: null }))
try {
const data = await fetcher()
set({ data, loading: false, error: null })
} catch (err) {
update((s) => ({
...s,
loading: false,
error: err instanceof Error ? err.message : 'Ошибка загрузки',
}))
}
},
reset() {
set({ data: null, loading: false, error: null })
},
}
}
export const productsStore = createAsyncStore<Product[]>()
export const products = derived(productsStore, ($s) => $s.data ?? [])
export const productsLoading = derived(productsStore, ($s) => $s.loading)
<script lang="ts">
import { onMount } from 'svelte'
import { productsStore, products, productsLoading } from '$lib/stores/products'
onMount(() => {
productsStore.load(() => fetch('/api/products').then((r) => r.json()))
})
</script>
{#if $productsLoading}
<Spinner />
{:else}
{#each $products as product (product.id)}
<ProductCard {product} />
{/each}
{/if}
Персистентность
// stores/theme.ts
import { writable } from 'svelte/store'
function persistedWritable<T>(key: string, initial: T) {
const stored = localStorage.getItem(key)
const value: T = stored ? JSON.parse(stored) : initial
const store = writable<T>(value)
store.subscribe((val) => {
localStorage.setItem(key, JSON.stringify(val))
})
return store
}
export const theme = persistedWritable<'light' | 'dark'>('theme', 'light')
export const language = persistedWritable<string>('lang', 'ru')
Чтение стора вне компонента
import { get } from 'svelte/store'
import { auth } from '$lib/stores/auth'
// в API-клиенте
async function apiRequest(url: string, options: RequestInit = {}) {
const { token } = get(auth)
return fetch(url, {
...options,
headers: {
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
'Content-Type': 'application/json',
},
})
}
Тестирование
import { get } from 'svelte/store'
import { cart, cartTotal } from '../stores/cart'
beforeEach(() => cart.clear())
test('addItem добавляет товар', () => {
cart.addItem({ id: '1', name: 'Test', price: 100 })
expect(get(cart)).toHaveLength(1)
expect(get(cartTotal)).toBe(100)
})
test('повторный addItem увеличивает quantity', () => {
cart.addItem({ id: '1', name: 'Test', price: 100 })
cart.addItem({ id: '1', name: 'Test', price: 100 })
const items = get(cart)
expect(items).toHaveLength(1)
expect(items[0].quantity).toBe(2)
expect(get(cartTotal)).toBe(200)
})
Структура файлов
src/lib/
stores/
auth.ts
cart.ts
ui.ts # тема, язык, sidebar
notifications.ts
index.ts # реэкспорт
Что делаем
Проектируем кастомные сторы под бизнес-логику приложения, реализуем async-паттерны, настраиваем персистентность, добавляем TypeScript-типизацию, покрываем тестами через Vitest.
Срок: 1–2 дня.







