11. Логические блоки (#if, #each, #await)
🧩 Логические блоки Svelte
Заголовок раздела «🧩 Логические блоки Svelte»Svelte — это не просто JavaScript с реактивностью. В шаблонах есть специальный синтаксис для условий, списков и асинхронного кода. Он намного мощнее, чем кажется с первого взгляда 🔥
{#if} — Условный рендеринг
Заголовок раздела «{#if} — Условный рендеринг»Самый простой блок. Отображает HTML только если условие истинно:
<script> let isLoggedIn = false let userRole = 'admin' let score = 85</script>
<!-- Простое условие -->{#if isLoggedIn} <p>Добро пожаловать!</p>{/if}
<!-- С else -->{#if isLoggedIn} <button>Выйти</button>{:else} <button>Войти</button>{/if}
<!-- Цепочка else if -->{#if score >= 90} <span>⭐ Отлично!</span>{:else if score >= 75} <span>👍 Хорошо</span>{:else if score >= 60} <span>😐 Удовлетворительно</span>{:else} <span>😢 Нужно подтянуться</span>{/if}
<!-- Вложенные условия -->{#if isLoggedIn} {#if userRole === 'admin'} <AdminPanel /> {:else if userRole === 'moderator'} <ModeratorPanel /> {:else} <UserDashboard /> {/if}{/if}{#if} vs CSS display: none
Заголовок раздела «{#if} vs CSS display: none»Важное отличие от v-if в Vue и v-show:
<!-- {#if} — DOM элемент СОЗДАЁТСЯ и УНИЧТОЖАЕТСЯ -->{#if showComponent} <ExpensiveComponent /> <!-- onMount/onDestroy вызываются! -->{/if}
<!-- Скрытие через CSS — DOM всегда в памяти --><ExpensiveComponent style="display: {showComponent ? 'block' : 'none'}" />Когда использовать {#if}:✅ Компонент редко показывается✅ Нужны onMount/onDestroy lifecycle hooks✅ Большой компонент — лучше не держать в DOM
Когда использовать display: none / visibility:✅ Компонент часто переключается (toggle)✅ Нужно сохранить состояние компонента✅ Анимации входа/выхода{#each} — Рендеринг списков
Заголовок раздела «{#each} — Рендеринг списков»Базовый синтаксис с индексом:
<script> let fruits = ['Яблоко', 'Банан', 'Апельсин']
interface User { id: number name: string online: boolean }
let users: User[] = [ { id: 1, name: 'Яша', online: true }, { id: 2, name: 'Петя', online: false }, { id: 3, name: 'Маша', online: true }, ]</script>
<!-- Простой список --><ul> {#each fruits as fruit} <li>{fruit}</li> {/each}</ul>
<!-- С индексом --><ol> {#each fruits as fruit, index} <li>{index + 1}. {fruit}</li> {/each}</ol>
<!-- Деструктуризация объектов -->{#each users as { id, name, online }} <div class="user" class:online> <span>{name}</span> <span>{online ? '🟢' : '⚫'}</span> </div>{/each}Ключи в {#each}: КРИТИЧЕСКИ ВАЖНО! 🔑
Заголовок раздела «Ключи в {#each}: КРИТИЧЕСКИ ВАЖНО! 🔑»Ключ — это третий параметр в скобках. Он говорит Svelte, как отождествлять элементы при изменении списка:
<!-- ❌ БЕЗ ключа — Svelte обновляет DOM позиционно -->{#each users as user} <UserCard {user} />{/each}
<!-- ✅ С ключом — Svelte знает КАКОЙ элемент обновить -->{#each users as user (user.id)} <UserCard {user} />{/each}Почему это важно? Представь список из 3 элементов [A, B, C]. Ты удаляешь B:
Без ключа: С ключом (id):Позиция 0: A → A id=1 (A): не трогаемПозиция 1: B → C id=2 (B): УДАЛЯЕМПозиция 2: C → ??? id=3 (C): не трогаемИтог: обновил 2 Итог: удалил 1<script> let todos = [ { id: 1, text: 'Купить продукты', done: false }, { id: 2, text: 'Написать код', done: true }, { id: 3, text: 'Погулять', done: false }, ]
function removeTodo(id: number) { todos = todos.filter(t => t.id !== id) }
function addTodo(text: string) { todos = [...todos, { id: Date.now(), text, done: false }] }</script>
<!-- ✅ Правильно: ключ = id -->{#each todos as todo (todo.id)} <div transition:slide> <input type="checkbox" bind:checked={todo.done} /> <span class:done={todo.done}>{todo.text}</span> <button on:click={() => removeTodo(todo.id)}>✕</button> </div>{/each}{:else} в {#each}
Заголовок раздела «{:else} в {#each}»Что показывать если список пустой:
<script> let searchResults: string[] = [] let isSearching = false</script>
{#each searchResults as result (result)} <div class="result">{result}</div>{:else} {#if isSearching} <div class="loading">⏳ Поиск...</div> {:else} <div class="empty">🔍 Ничего не найдено</div> {/if}{/each}Вложенные {#each}
Заголовок раздела «Вложенные {#each}»<script> let categories = [ { name: 'Фрукты', items: ['Яблоко', 'Банан', 'Манго'], }, { name: 'Овощи', items: ['Морковь', 'Брокколи', 'Лук'], }, ]</script>
{#each categories as category (category.name)} <section> <h3>{category.name}</h3> <ul> {#each category.items as item} <li>{item}</li> {/each} </ul> </section>{/each}{#await} — Асинхронный рендеринг 🕐
Заголовок раздела «{#await} — Асинхронный рендеринг 🕐»Svelte умеет работать с Promise прямо в шаблоне:
<script> // Просто возвращаем промис! async function loadUser(id: number) { const res = await fetch(`/api/users/${id}`) if (!res.ok) throw new Error('Пользователь не найден') return res.json() }
let userId = 1 $: userPromise = loadUser(userId) // Реактивно!</script>
{#await userPromise} <!-- Пока загружается --> <div class="loading"> <Spinner /> <p>Загружаем пользователя #{userId}...</p> </div>{:then user} <!-- Успешная загрузка --> <div class="user-card"> <img src={user.avatar} alt={user.name} /> <h2>{user.name}</h2> <p>{user.email}</p> </div>{:catch error} <!-- Ошибка --> <div class="error"> <p>❌ {error.message}</p> <button on:click={() => userId = userId}>Попробовать снова</button> </div>{/await}{#await} — Краткая форма
Заголовок раздела «{#await} — Краткая форма»Если ошибка не нужна, можно писать короче:
<script> let promise = fetch('/api/data').then(r => r.json())</script>
<!-- Краткая форма: только loading + then -->{#await promise} <p>⏳ Загрузка...</p>{:then data} <pre>{JSON.stringify(data, null, 2)}</pre>{/await}
<!-- Ещё короче: только результат (без loading state) -->{#await promise then data} <pre>{JSON.stringify(data, null, 2)}</pre>{/await}
<!-- Обработка только ошибки -->{#await promise catch error} <p>Ошибка: {error.message}</p>{/await}{#await} с реактивным промисом
Заголовок раздела «{#await} с реактивным промисом»<script> import { writable } from 'svelte/store'
const postId = writable(1)
// Промис пересоздаётся при изменении $postId! $: post = fetch( `https://jsonplaceholder.typicode.com/posts/${$postId}` ).then(r => r.json())</script>
<div class="controls"> <button on:click={() => $postId--} disabled={$postId <= 1}>← Назад</button> <span>Пост #{$postId}</span> <button on:click={() => $postId++}>Вперёд →</button></div>
{#await post} <div class="skeleton">Загружаем пост...</div>{:then data} <article> <h2>{data.title}</h2> <p>{data.body}</p> </article>{:catch err} <p class="error">Ошибка: {err.message}</p>{/await}{#snippet} — Svelte 5: Переиспользуемые блоки 🆕
Заголовок раздела «{#snippet} — Svelte 5: Переиспользуемые блоки 🆕»В Svelte 5 появились snippets — именованные фрагменты шаблона внутри компонента. Это мощная замена слотов!
<!-- Svelte 5 --><script> let items = ['Яблоко', 'Банан', 'Манго']</script>
<!-- Объявляем snippet -->{#snippet fruitItem(fruit: string, index: number)} <li class="fruit-item"> <span class="index">{index + 1}.</span> <span class="name">{fruit}</span> <span class="emoji">🍎</span> </li>{/snippet}
<!-- Используем с @render --><ul> {#each items as fruit, i} {@render fruitItem(fruit, i)} {/each}</ul>{@render} — Svelte 5: Рендер сниппета
Заголовок раздела «{@render} — Svelte 5: Рендер сниппета»<script lang="ts"> interface TableColumn { key: string label: string }
interface User { id: number name: string email: string role: string }
let { columns, rows } = $props<{ columns: TableColumn[] rows: User[] }>()</script>
<!-- Snippet для заголовка колонки -->{#snippet columnHeader(col: TableColumn)} <th class="header-cell"> <span>{col.label}</span> <button class="sort-btn">↕</button> </th>{/snippet}
<!-- Snippet для ячейки данных -->{#snippet dataCell(value: unknown)} <td class="data-cell"> {String(value)} </td>{/snippet}
<table> <thead> <tr> {#each columns as col} {@render columnHeader(col)} {/each} </tr> </thead> <tbody> {#each rows as row} <tr> {#each columns as col} {@render dataCell(row[col.key as keyof User])} {/each} </tr> {/each} </tbody></table>Snippets как пропсы компонента (Svelte 5)
Заголовок раздела «Snippets как пропсы компонента (Svelte 5)»<!-- Card.svelte - принимает snippet как проп --><script lang="ts"> import type { Snippet } from 'svelte'
let { title, children, footer, } = $props<{ title: string children: Snippet footer?: Snippet<[{ onClose: () => void }]> }>()
let visible = $state(true)</script>
{#if visible} <div class="card"> <h2>{title}</h2> <div class="content"> {@render children()} </div> {#if footer} <div class="footer"> {@render footer({ onClose: () => visible = false })} </div> {/if} </div>{/if}<!-- Использование Card.svelte --><Card title="Моя карточка"> <p>Содержимое карточки</p>
{#snippet footer({ onClose })} <button on:click={onClose}>Закрыть ✕</button> <button>Сохранить ✅</button> {/snippet}</Card>Сравнение Svelte 4 vs Svelte 5 логических блоков
Заголовок раздела «Сравнение Svelte 4 vs Svelte 5 логических блоков»Svelte 4: Svelte 5:──────────────────────────────────────────────────{#if x} ... {/if} {#if x} ... {/if} ← БЕЗ ИЗМЕНЕНИЙ
{#each items as i} ... {#each items as i} ... ← БЕЗ ИЗМЕНЕНИЙ
{#await promise}... {#await promise}... ← БЕЗ ИЗМЕНЕНИЙ
<slot /> {@render children()} ← ИЗМЕНИЛОСЬ!<slot name="header" /> {@render header()} ← ИЗМЕНИЛОСЬ!
Нет аналога {#snippet name()} ← НОВОЕ!Практические паттерны
Заголовок раздела «Практические паттерны»Паттерн: Loading/Error/Success
Заголовок раздела «Паттерн: Loading/Error/Success»<script> let state: 'idle' | 'loading' | 'success' | 'error' = 'idle' let data: unknown = null let errorMessage = ''
async function loadData() { state = 'loading' try { const res = await fetch('/api/data') data = await res.json() state = 'success' } catch (e) { errorMessage = (e as Error).message state = 'error' } }</script>
{#if state === 'idle'} <button on:click={loadData}>Загрузить данные</button>{:else if state === 'loading'} <Spinner />{:else if state === 'success'} <DataDisplay {data} />{:else if state === 'error'} <ErrorMessage message={errorMessage} onRetry={loadData} />{/if}Паттерн: Пустое состояние
Заголовок раздела «Паттерн: Пустое состояние»<script> let items: string[] = []</script>
{#if items.length === 0} <EmptyState icon="📭" title="Пока ничего нет" description="Добавьте первый элемент" > <button slot="action" on:click={addItem}> + Добавить </button> </EmptyState>{:else} {#each items as item (item)} <ItemCard {item} /> {/each}{/if}