15. Context API
🌳 Context API в Svelte
Заголовок раздела «🌳 Context API в Svelte»Context — это способ передавать данные вниз по дереву компонентов без пропс-дриллинга. Представь: есть данные пользователя, нужные в кнопке в глубине компонентного дерева. Прокидывать через 5 уровней пропсов? Или один раз setContext в корне? 🎯
Проблема без Context
Заголовок раздела «Проблема без Context»App└── Layout └── Dashboard └── Sidebar └── UserMenu └── Avatar ← нужны данные пользователя!
Без context: user → Layout → Dashboard → Sidebar → UserMenu → Avatar ↑ Props drilling — неудобно и хрупко!
С context: setContext в App → getContext в Avatar напрямуюsetContext и getContext
Заголовок раздела «setContext и getContext»<script lang="ts"> import { setContext } from 'svelte'
const user = { name: 'Яша', role: 'admin', avatar: '/avatar.jpg', }
// Устанавливаем контекст — будет доступен всем потомкам! setContext('user', user) setContext('theme', 'dark')
// Ключ может быть любым: строка, Symbol, объект const CONFIG_KEY = Symbol('config') setContext(CONFIG_KEY, { apiUrl: '/api/v1' })</script>
<slot /><!-- DeepChild.svelte — в глубине дерева --><script lang="ts"> import { getContext } from 'svelte'
// Получаем контекст по ключу const user = getContext<{ name: string; role: string }>('user') const theme = getContext<'light' | 'dark'>('theme')
// Если ключ не найден — возвращает undefined // (не выбрасывает ошибку!)</script>
<div class="profile"> <img src="/avatar.jpg" alt={user.name} /> <span>{user.name} ({user.role})</span></div>hasContext и getAllContexts
Заголовок раздела «hasContext и getAllContexts»<script lang="ts"> import { hasContext, getAllContexts } from 'svelte'
// Проверить наличие контекста if (hasContext('user')) { const user = getContext('user') // безопасно! }
// Получить все контексты (для отладки) const allContexts = getAllContexts() console.log('Все контексты:', allContexts) // Map { 'user' => {...}, 'theme' => 'dark', ... }</script>Context vs Stores: ключевые отличия
Заголовок раздела «Context vs Stores: ключевые отличия»┌──────────────────────┬──────────────────────┬──────────────────────┐│ │ Context │ Stores │├──────────────────────┼──────────────────────┼──────────────────────┤│ Область видимости │ Компонентное дерево │ Всё приложение ││ Реактивность │ ❌ Нет │ ✅ Да ││ SSR │ ✅ Безопасен │ ⚠️ Нужен осторожный ││ Количество экземпл. │ ✅ Несколько │ Обычно один ││ Тестирование │ Нужна обёртка │ Легко мокать │└──────────────────────┴──────────────────────┴──────────────────────┘Используй Context когда:✅ Данные нужны только в поддереве (не всему приложению)✅ Несколько экземпляров компонента с разными данными✅ SSR — каждый запрос должен иметь свой контекст✅ Compound components (Accordion + AccordionItem)
Используй Stores когда:✅ Глобальное состояние (авторизация, корзина)✅ Нужна реактивность✅ Нужно изменять состояние из разных местContext + Stores = 🔥
Заголовок раздела «Context + Stores = 🔥»Лучший паттерн: хранить store в контексте. Это даёт и реактивность, и изоляцию!
<script lang="ts"> import { setContext } from 'svelte' import { writable } from 'svelte/store' import type { Writable } from 'svelte/store'
type Theme = 'light' | 'dark' | 'system'
// Создаём store и помещаем в контекст const theme: Writable<Theme> = writable('dark')
setContext('theme', { theme, toggleTheme: () => { theme.update(t => t === 'dark' ? 'light' : 'dark') }, setTheme: (t: Theme) => theme.set(t), })</script>
<slot /><!-- Любой потомок может читать И изменять тему! --><script lang="ts"> import { getContext } from 'svelte' import type { Writable } from 'svelte/store'
const { theme, toggleTheme } = getContext<{ theme: Writable<'light' | 'dark'> toggleTheme: () => void }>('theme')
// $theme — реактивно! Обновляется автоматически</script>
<button on:click={toggleTheme}> Тема: {$theme === 'dark' ? '🌙' : '☀️'}</button>TypeScript: Типизация контекста с Symbol 🔐
Заголовок раздела «TypeScript: Типизация контекста с Symbol 🔐»// context/types.ts — централизованные ключи и типы
import type { Writable } from 'svelte/store'
// Используем Symbol как ключ — уникально и типобезопасноexport const THEME_KEY = Symbol('theme')export const AUTH_KEY = Symbol('auth')export const ROUTER_KEY = Symbol('router')
// Интерфейсы для каждого контекстаexport interface ThemeContext { theme: Writable<'light' | 'dark'> toggleTheme: () => void}
export interface AuthContext { user: Writable<User | null> login: (email: string, password: string) => Promise<void> logout: () => Promise<void> isLoading: Writable<boolean>}
export interface RouterContext { currentPath: Writable<string> navigate: (path: string) => void params: Writable<Record<string, string>>}
// Хелперы для типобезопасного get/setContextimport { getContext, setContext } from 'svelte'
export function setThemeContext(ctx: ThemeContext) { setContext(THEME_KEY, ctx)}
export function getThemeContext(): ThemeContext { return getContext(THEME_KEY)}
export function setAuthContext(ctx: AuthContext) { setContext(AUTH_KEY, ctx)}
export function getAuthContext(): AuthContext { return getContext(AUTH_KEY)}<!-- Использование типизированного контекста --><script lang="ts"> import { setAuthContext, getAuthContext } from './context/types' import { writable } from 'svelte/store'
// В провайдере const user = writable<User | null>(null) const isLoading = writable(false)
setAuthContext({ user, isLoading, async login(email, password) { isLoading.set(true) try { const userData = await api.login(email, password) user.set(userData) } finally { isLoading.set(false) } }, async logout() { await api.logout() user.set(null) } })</script>Context в SvelteKit
Заголовок раздела «Context в SvelteKit»В SvelteKit контекст особенно важен для SSR, т.к. каждый запрос — новый экземпляр:
<script lang="ts"> import { setContext } from 'svelte' import { writable } from 'svelte/store'
export let data // Данные от load()
// Создаём stores из данных layout // Каждый запрос получает СВОИ stores! const user = writable(data.user) const session = writable(data.session)
setContext('user', user) setContext('session', session)
// Обновляем при навигации $: user.set(data.user) $: session.set(data.session)</script>
<slot />import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => { return { user: locals.user ?? null, session: locals.session ?? null, }}Паттерн: Theme Context
Заголовок раздела «Паттерн: Theme Context»<!-- ThemeProvider.svelte — полная реализация --><script lang="ts"> import { setContext, onMount } from 'svelte' import { writable, derived } from 'svelte/store'
type ThemeMode = 'light' | 'dark' | 'system'
const mode = writable<ThemeMode>('system') const systemDark = writable(false)
const isDark = derived( [mode, systemDark], ([$mode, $systemDark]) => { if ($mode === 'system') return $systemDark return $mode === 'dark' } )
// Применяем тему к document isDark.subscribe($isDark => { document.documentElement.classList.toggle('dark', $isDark) })
onMount(() => { // Определяем системную тему const mq = window.matchMedia('(prefers-color-scheme: dark)') systemDark.set(mq.matches) mq.addEventListener('change', e => systemDark.set(e.matches))
// Читаем сохранённую тему const saved = localStorage.getItem('theme') as ThemeMode | null if (saved) mode.set(saved)
return () => mq.removeEventListener('change', () => {}) })
setContext('theme', { mode, isDark, setMode: (m: ThemeMode) => { mode.set(m) localStorage.setItem('theme', m) }, })</script>
<slot />Паттерн: Auth Context
Заголовок раздела «Паттерн: Auth Context»<script lang="ts"> import { setContext } from 'svelte' import { writable, derived } from 'svelte/store'
interface User { id: string name: string email: string role: 'user' | 'admin' }
const user = writable<User | null>(null) const loading = writable(true)
const isAuthenticated = derived(user, $user => $user !== null) const isAdmin = derived(user, $user => $user?.role === 'admin')
// Инициализация — проверяем сессию async function init() { try { const response = await fetch('/api/auth/me') if (response.ok) { const userData = await response.json() user.set(userData) } } catch { user.set(null) } finally { loading.set(false) } }
init()
setContext('auth', { user, loading, isAuthenticated, isAdmin,
async login(email: string, password: string) { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const userData = await res.json() user.set(userData) },
async logout() { await fetch('/api/auth/logout', { method: 'POST' }) user.set(null) }, })</script>
<slot /><!-- Кнопка выхода в глубине дерева --><script> import { getContext } from 'svelte'
const { user, logout, isAdmin } = getContext('auth')</script>
{#if $user} <div class="user-info"> <span>Привет, {$user.name}!</span> {#if $isAdmin} <span class="badge">Admin</span> {/if} <button on:click={logout}>Выйти</button> </div>{/if}