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

15. Context API

Context — это способ передавать данные вниз по дереву компонентов без пропс-дриллинга. Представь: есть данные пользователя, нужные в кнопке в глубине компонентного дерева. Прокидывать через 5 уровней пропсов? Или один раз setContext в корне? 🎯


App
└── Layout
└── Dashboard
└── Sidebar
└── UserMenu
└── Avatar ← нужны данные пользователя!
Без context: user → Layout → Dashboard → Sidebar → UserMenu → Avatar
↑ Props drilling — неудобно и хрупко!
С context: setContext в App → getContext в Avatar напрямую

ParentComponent.svelte
<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>

<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 │ Stores │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Область видимости │ Компонентное дерево │ Всё приложение │
│ Реактивность │ ❌ Нет │ ✅ Да │
│ SSR │ ✅ Безопасен │ ⚠️ Нужен осторожный │
│ Количество экземпл. │ ✅ Несколько │ Обычно один │
│ Тестирование │ Нужна обёртка │ Легко мокать │
└──────────────────────┴──────────────────────┴──────────────────────┘
Используй Context когда:
✅ Данные нужны только в поддереве (не всему приложению)
✅ Несколько экземпляров компонента с разными данными
✅ SSR — каждый запрос должен иметь свой контекст
✅ Compound components (Accordion + AccordionItem)
Используй Stores когда:
✅ Глобальное состояние (авторизация, корзина)
✅ Нужна реактивность
✅ Нужно изменять состояние из разных мест

Лучший паттерн: хранить store в контексте. Это даёт и реактивность, и изоляцию!

ThemeProvider.svelte
<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>

// 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/setContext
import { 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>

В SvelteKit контекст особенно важен для SSR, т.к. каждый запрос — новый экземпляр:

+layout.svelte
<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 />
+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user ?? null,
session: locals.session ?? null,
}
}

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

AuthProvider.svelte
<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}