Продвинутые паттерны Svelte для сложных приложений
Управление состоянием
Управление состоянием в Svelte
Когда приложение растет, передача данных через свойства (props) от родительского компонента к дочернему становится громоздкой. Это явление называют «пробросом свойств» (prop drilling). Чтобы избежать этого и сделать управление данными более чистым и эффективным, в Svelte есть два мощных инструмента: хранилища (stores) и контекст (context).
Хранилища для глобального состояния
Хранилище — это объект с методом subscribe, который позволяет компонентам получать уведомления об изменениях значения. Представьте его как центральный источник данных, на который может «подписаться» любой компонент в приложении.
В Svelte есть несколько видов встроенных хранилищ.
writable(записываемое) хранилище: Самый распространенный тип. Его значение можно изменять как изнутри, так и снаружи компонента. Для этого используются методыset(установить новое значение) иupdate(обновить на основе текущего).
import { writable } from 'svelte/store';
// Создаем хранилище со значением по умолчанию 0
export const count = writable(0);
Чтобы использовать это хранилище в компоненте, мы импортируем его и подписываемся на изменения. Самый простой способ сделать это — использовать префикс $.
<script>
import { count } from './stores.js';
function increment() {
// $count — это сокращение для получения значения
// и автоматической подписки/отписки.
// Мы можем присваивать ему значение напрямую.
$count += 1;
}
</script>
<h1>Счетчик: {$count}</h1>
<button on:click={increment}>+</button>
Такой подход автоматически управляет подпиской. Компонент будет перерисовываться каждый раз, когда значение $count меняется, и отпишется, когда компонент будет уничтожен.
readable (читаемое) хранилище:
Это хранилище, значение которого нельзя изменить извне. Оно устанавливается при создании и может обновляться только изнутри. Это полезно для данных, которые должны быть защищены от случайных изменений, например, таймер или данные от веб-сокета.
import { readable } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
// Эта функция выполняется, когда появляется первый подписчик
const interval = setInterval(() => {
set(new Date());
}, 1000);
// Эта функция выполняется, когда уходит последний подписчик
return function stop() {
clearInterval(interval);
};
});
derived (производное) хранилище:
Производное хранилище получает свое значение от одного или нескольких других хранилищ. Оно полезно, когда нужно вычислить какое-то значение на основе другого состояния, не создавая лишней логики в компонентах.
import { readable, derived } from 'svelte/store';
const startTime = readable(new Date().getTime());
export const elapsed = derived(
startTime,
$startTime => Math.round((new Date().getTime() - $startTime) / 1000)
);
// elapsed будет автоматически обновляться, но мы не можем
// его обновить напрямую. В этом примере оно не будет меняться,
// т.к. startTime не меняется. Но если бы startTime было writable,
// elapsed бы обновлялось при каждом изменении startTime.
Контекст для иерархии компонентов
Контекст — это механизм для передачи данных вниз по дереву компонентов без необходимости передавать их через каждый уровень. Это похоже на предоставление общей информации для целой ветки компонентов.
В отличие от хранилищ, контекст не является реактивным. Если значение в контексте изменится, компоненты, которые его используют, не обновятся автоматически. Поэтому контекст лучше всего подходит для передачи данных, которые не меняются, например, функций или объектов с настройками.
Для работы с контекстом используются две функции: setContext и getContext.
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte';
import Child from './Child.svelte';
const someData = {
user: 'Alex',
theme: 'dark'
};
// Устанавливаем контекст. Ключ 'app-data' — любой уникальный идентификатор.
setContext('app-data', someData);
</script>
<Child />
Любой дочерний компонент внутри Parent.svelte, даже если он вложен на несколько уровней, может получить доступ к этим данным.
<!-- DeeplyNestedChild.svelte -->
<script>
import { getContext } from 'svelte';
// Получаем данные по тому же ключу
const appData = getContext('app-data');
</script>
<p>Пользователь: {appData.user}</p>
<p>Тема: {appData.theme}</p>
Основное правило: используйте хранилища для глобального, реактивного состояния, а контекст — для передачи статических данных или функций внутри определенной иерархии компонентов.
Интеграция с внешними библиотеками
Хотя встроенные инструменты Svelte покрывают большинство потребностей, иногда проект требует интеграции с более сложными решениями, такими как XState для конечных автоматов или Immer для работы с иммутабельными данными.
Благодаря гибкости Svelte, интегрировать их довольно просто. Часто для этого достаточно обернуть логику внешней библиотеки в кастомное Svelte-хранилище.
Например, чтобы интегрировать XState, можно создать кастомное хранилище, которое будет управлять машиной состояний и уведомлять подписчиков об изменениях.
import { readable } from 'svelte/store';
import { createMachine, interpret } from 'xstate';
const machine = createMachine(/* ...конфигурация машины... */);
export const myService = readable(machine.initialState, set => {
const service = interpret(machine).onTransition(state => {
if (state.changed) {
set(state);
}
});
service.start();
return () => service.stop();
});
Такой подход позволяет использовать всю мощь внешних библиотек, сохраняя при этом идиоматичный и реактивный стиль Svelte.
Используйте систему хранилищ Svelte для элегантной обработки переходов состояний без шаблонного кода.
Выбирая между встроенными инструментами и внешними библиотеками, всегда начинайте с простого. Хранилища Svelte удивительно мощны и могут справиться с большинством задач по управлению состоянием в сложных приложениях.
Какая основная проблема в больших приложениях Svelte решается с помощью хранилищ (stores) и контекста (context)?
Какое ключевое отличие контекста (context) от хранилища (store) в Svelte?
Правильное управление состоянием — ключ к созданию масштабируемых и поддерживаемых приложений. Понимание разницы между хранилищами и контекстом, а также умение интегрировать внешние инструменты, когда это необходимо, делает вас более эффективным разработчиком.