Svelte 5: Runes для оптимистичных мутаций в реальном времени
-

Svelte 5 принёс радикальные изменения в реактивность - и это не просто очередной мейджор апдейт. Теперь у нас есть руны, которые переносят логику состояния прямо в компонент без лишних обёрток. Это открывает невероятные возможности для реализации оптимистичных мутаций, которые раньше требовали жонглирования состояниями и сложной синхронизацией.
Если ты когда-нибудь писала код, где пользователь кликает, UI обновляется, а потом ждёшь ответа с сервера - ты знаешь эту боль. Оптимистичные обновления решают эту проблему: меняешь интерфейс сейчас, а не когда бекенд решит ответить. В этой статье разберёмся, как Svelte 5 рунами и TanStack Query делают это elegantly.
Что такое руны и почему они меняют игру
Руны в Svelte 5 - это по сути магические функции-компилятор-сигналы, которые влияют на работу компилятора напрямую. Вместо того чтобы писать громоздкий код с
useStateилиuseReducer, ты просто пишешь обычный JavaScript, а компилятор сам понимает, где нужна реактивность.В предыдущих версиях Svelte для создания реактивного состояния ты писала простой
let, и компилятор это понимал. Но в Svelte 5 пошли дальше - теперь у тебя есть явные руны вроде$state,$derived,$effect, которые дают больше контроля и предсказуемости. Это не просто синтаксический сахар - это фундаментальный сдвиг в архитектуре реактивности.Ключевые руны для работы с состоянием:
- $state - создаёт реактивное состояние, которое компилятор будет отслеживать. Можешь использовать примитивы или объекты - всё равно работает;
- $derived - вычисляемое состояние, которое автоматически обновляется, когда меняются зависимости (аналог
useMemo); - $effect - побочные эффекты, которые запускаются при изменении зависимостей (аналог
useEffect); - $props - явная передача пропсов в компонент с типизацией.
Зачем это нужно? Потому что мелкозернистая реактивность - это киллер-фича. Svelte 5 использует сигналы для реактивности, как и Solid.js. Это означает, что если в большом списке из 1000 элементов изменится один - перерисуется только он, а не весь компонент. Производительность просто зашкаливает.
$state для оптимистичных обновлений
Теперь к самому интересному. Для оптимистичных мутаций нужно управлять несколькими состояниями одновременно: локальное состояние пользователя, статус загрузки, возможный откат. Раньше это была боль - нужно было синхронизировать множество
useStateхуков.С Svelte 5 это становится элегантнее. Вот как выглядит базовая структура:
<script> let todos = $state([]); let isLoading = $state(false); let error = $state(null); async function addTodoOptimistic(text) { const optimisticTodo = { id: Math.random(), text, pending: true }; // Оптимистичное обновление - сразу показываем пользователю todos.push(optimisticTodo); isLoading = true; try { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) }); if (!response.ok) throw new Error('Ошибка сервера'); const newTodo = await response.json(); // Заменяем оптимистичное на реальное todos = todos.map(t => t.id === optimisticTodo.id ? newTodo : t ); } catch (err) { // Откат при ошибке todos = todos.filter(t => t.id !== optimisticTodo.id); error = err.message; } finally { isLoading = false; } } </script>Это уже намного чище, чем в классическом React. Но подожди, мы можем ещё лучше!
TanStack Query + Svelte 5 рунами
TanStack Query (бывший React Query) - это супер-библиотека для управления асинхронным состоянием. Она берёт на себя кеширование, синхронизацию, инвалидацию, и предоставляет удобный API для работы с этим.
Свежая новость - TanStack Query работает не только с React. Есть адаптер для Svelte, который отлично интегрируется с рунами. Вот как это выглядит в Svelte 5:
import { createMutation, useQueryClient } from '@tanstack/svelte-query'; let queryClient = useQueryClient(); const mutation = createMutation({ mutationFn: async (newTodo) => { const res = await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }); return res.json(); }, onMutate: async (newTodo) => { // Отменяем любые запросы на получение todos await queryClient.cancelQueries({ queryKey: ['todos'] }); // Сохраняем старые данные для отката const previousTodos = queryClient.getQueryData(['todos']); // Оптимистичное обновление кеша queryClient.setQueryData(['todos'], (old) => [ ...old, { ...newTodo, id: Date.now(), isPending: true } ]); return { previousTodos }; }, onError: (err, newTodo, context) => { // Откат на старые данные при ошибке queryClient.setQueryData(['todos'], context.previousTodos); }, onSuccess: (data) => { // Инвалидируем кеш, чтобы получить свежие данные queryClient.invalidateQueries({ queryKey: ['todos'] }); } });Видишь, как это работает? TanStack Query берёт на себя:**
- onMutate - предварительная обработка перед запросом (оптимистичное обновление);
- onError - откат при ошибке;
- onSuccess - что делать после успешного ответа;
- Кеширование - автоматическая синхронизация данных между запросами.
Это жорстко лучше, чем писать всё вручную. TanStack Query заботится о граничных случаях, которые ты забудешь учесть.
useOptimistic для UI-паттернов
Хотя в Svelte 5 нет встроенного
useOptimistic(как в React 19), ты можешь легко создать свой хук, используя руны. Идея в том, что ты показываешь оптимистичное состояние UI, пока идёт запрос, а потом синхронизируешь с реальностью.Вот паттерн, который работает отлично:
<script> import { createMutation } from '@tanstack/svelte-query'; let todos = $state([]); let optimisticTodos = $derived.by(() => { return todos.map(t => ({ ...t, isOptimistic: !t.confirmed })); }); const deleteMutation = createMutation({ mutationFn: (id) => fetch(`/api/todos/${id}`, { method: 'DELETE' }), onMutate: (id) => { // Мгновенно удаляем из UI todos = todos.filter(t => t.id !== id); return id; }, onError: (err, id, context) => { // Возвращаем назад, если ошибка todos.push({ id, text: 'Ошибка удаления', confirmed: false }); } }); function handleDeleteClick(id) { deleteMutation.mutate(id); } </script> <ul> {#each optimisticTodos as todo (todo.id)} <li class:pending={todo.isOptimistic}> {todo.text} <button on:click={() => handleDeleteClick(todo.id)}> Удалить </button> </li> {/each} </ul> <style> .pending { opacity: 0.6; text-decoration: line-through; } </style>Видишь, как
$derivedпомогает? Это вычисляемое состояние автоматически обновляется, когда меняетсяtodos. Никаких лишних рендеров, всё мелкозернисто.Сравнение подходов
Подход Плюсы Минусы Когда использовать Ручное управление состоянием Полный контроль, минимальные зависимости Много кода, легко ошибиться, hard to debug Простые формы, прототипы TanStack Query + Svelte 5 Кеширование, синхронизация, встроенная обработка ошибок Ещё одна зависимость, learning curve Production-приложения, сложные данные Combobox: Руны + собственный хук Легковесно, всё под контролем, можешь использовать TypeScript Нужно тестировать, поддерживать Специфичные кейсы, микро-оптимизации Практические советы и пидоры
Когда ты работаешь с оптимистичными обновлениями, есть несколько подводных камней, в которые легко наступить:
- Конфликты идентификаторов - если генерируешь ID на клиенте (например,
Math.random()), они могут конфликтовать. Лучше используй UUID или синхронизируй ID после успешного создания на сервере; - Race conditions - если пользователь отправляет запрос, а потом тут же отправляет второй, нужно убедиться, что они применяются в правильном порядке. TanStack Query это обрабатывает, но собственный код - может упасть;
- Offline-режим - если соединение порвалось, мутация повиснет. Нужно обработать timeout и предложить пользователю повторить;
- Visibility state - используй
document.visibilityStateдля отслеживания, виден ли пользователю вкладка. Если невидима - можешь отложить рефетч.
Продвинутый паттерн - оптимистичное обновление с очередью мутаций:
let mutationQueue = $state([]); let isProcessing = $state(false); async function processMutationQueue() { if (isProcessing || mutationQueue.length === 0) return; isProcessing = true; while (mutationQueue.length > 0) { const mutation = mutationQueue.shift(); try { await mutation.fn(); } catch (err) { // Откат для этой мутации mutation.rollback?.(); } } isProcessing = false; }Это гарантирует, что мутации выполняются по очереди, даже если пользователь спамит кнопку.
Поспешу наслаждаться результатом
Svelte 5 с рунами переносит нас в будущее, где реактивность - это не магия, а явный контроль. TanStack Query превращает сложное управление асинхронным состоянием в простую конфигурацию. Вместе они создают идеальную среду для оптимистичных мутаций, которые раньше требовали уйму boilerplate.
Остаётся только помнить про edge cases и не забыть про типизацию TypeScript - и тогда твой код будет не просто работающим, но и красивым. Код, который хочется перечитывать, а не от него бежать.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.