Перейти к содержимому

11. Логические блоки (#if, #each, #await)

Svelte — это не просто JavaScript с реактивностью. В шаблонах есть специальный синтаксис для условий, списков и асинхронного кода. Он намного мощнее, чем кажется с первого взгляда 🔥


Самый простой блок. Отображает 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}

Важное отличие от 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)
✅ Нужно сохранить состояние компонента
✅ Анимации входа/выхода

Базовый синтаксис с индексом:

<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}

Ключ — это третий параметр в скобках. Он говорит 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}

Что показывать если список пустой:

<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}

<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}

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}

Если ошибка не нужна, можно писать короче:

<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}

<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}

В 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>

<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>

<!-- 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: 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()} ← НОВОЕ!

<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}