Die normale Context-API legt einen Wert ab — der bleibt dann so. Was aber, wenn der Wert sich über die Zeit ändern soll? Theme-Wechsel, Login-Status, Form-Validierung. Die Antwort ist im Grunde simpel: Du legst einen reaktiven Container in den Context, keine Kopie. Was im Container steht, kann sich beliebig oft ändern — alle Konsumenten reagieren automatisch. Dieser Artikel zeigt, wie das praktisch aussieht.

Warum man keine primitive Variable in Context legt

Kurz das Problem zur Erinnerung. Diese Variante sieht erst mal richtig aus:

svelte Funktioniert nicht
<script>
    import { setContext } from 'svelte';

    let theme = $state('light');
    setContext('theme', theme); // legt den Wert 'light' ab
</script>

Der Aufruf setContext('theme', theme) legt den aktuellen Wert von theme (also 'light') im Context ab. Was später mit der Variable passiert, ist dem Context egal — er hat ja nur den damaligen String gespeichert. Konsumenten sehen 'light' bis in alle Ewigkeit.

Was du brauchst, ist ein Container, dessen Inhalt veränderlich ist und dessen Identität (Referenz) gleich bleibt. Es gibt drei sehr saubere Wege dafür.

Weg 1 – $state-Objekt als Container

Die einfachste Form: Du verpackst den Wert in ein Objekt. Das Objekt selbst bleibt dieselbe Instanz, sein value-Feld ist reaktiv.

svelte App.svelte
<script>
    import { setContext } from 'svelte';

    const theme = $state({ value: 'light' });
    setContext('theme', theme);

    function toggle() {
        theme.value = theme.value === 'light' ? 'dark' : 'light';
    }
</script>

<button onclick={toggle}>Theme: {theme.value}</button>
<slot />
svelte ThemedBox.svelte (irgendwo unten)
<script>
    import { getContext } from 'svelte';

    const theme = getContext('theme');
</script>

<div class={`box theme-${theme.value}`}>

</div>

Wichtig zu beobachten: Der Konsument schreibt theme.value — nicht theme allein. Beim Lesen einer Property eines $state-Proxys merkt sich Svelte den Zugriff und aktualisiert genau diese Stelle, wenn das Property sich ändert.

Weg 2 – Klasse mit $state-Property

Wenn der Container nicht nur einen Wert, sondern auch passende Methoden mitbringen soll, ist eine Klasse oft eleganter.

ts src/lib/contexts/theme.svelte.ts
import { setContext, getContext } from 'svelte';

export class Theme {
    value = $state<'light' | 'dark'>('light');

    toggle() {
        this.value = this.value === 'light' ? 'dark' : 'light';
    }

    set(next: 'light' | 'dark') {
        this.value = next;
    }
}

const key = Symbol('theme');

export function provideTheme() {
    const theme = new Theme();
    setContext(key, theme);
    return theme;
}

export function useTheme(): Theme {
    const theme = getContext<Theme | undefined>(key);
    if (!theme) {
        throw new Error('Theme-Context wurde nicht gesetzt');
    }
    return theme;
}
svelte App.svelte
<script>
    import { provideTheme } from '$lib/contexts/theme.svelte';

    const theme = provideTheme();
</script>

<button onclick={() => theme.toggle()}>
    Theme: {theme.value}
</button>

<slot />
svelte ThemedBox.svelte
<script>
    import { useTheme } from '$lib/contexts/theme.svelte';

    const theme = useTheme();
</script>

<div class={`box theme-${theme.value}`}>

</div>

Drei Vorteile gegenüber dem rohen Objekt:

  • Klare API. Die Komponenten sagen theme.toggle() statt theme.value = ....
  • Type-Safety. TypeScript versteht den Klassen-Typ ohne weitere Annotation.
  • Wiederverwendbarkeit. Die Klasse selbst kann auch außerhalb von Context (z. B. in Tests) verwendet werden.

Weg 3 – Klassische Stores im Context

Wenn dein Code aus Svelte-4-Zeiten stammt oder du eine Library benutzt, die Stores liefert, kannst du auch einen Store in den Context legen.

svelte App.svelte
<script>
    import { writable } from 'svelte/store';
    import { setContext } from 'svelte';

    const theme = writable('light');
    setContext('theme', theme);
</script>
svelte ThemedBox.svelte
<script>
    import { getContext } from 'svelte';
    const theme = getContext('theme');
</script>

<div class={`box theme-${$theme}`}>…</div>

Funktioniert, ist aber etwas redundant in Runes-Code: Stores haben ihre eigene Subscribe-Mechanik, die $state-Container nicht braucht. Für neuen Code sind Weg 1 oder Weg 2 üblicher.

Praxis-Beispiel: Form-Kontext

Ein klassisches Anwendungsmuster: Eine <Form>-Komponente bietet ihren <Field>-Kindern einen gemeinsamen Validierungs-Container an, ohne dass dazwischen Props durchgereicht werden müssen.

ts src/lib/contexts/form.svelte.ts
import { setContext, getContext } from 'svelte';

export class FormContext {
    errors = $state<Record<string, string>>({});
    touched = $state<Record<string, boolean>>({});

    setError(field: string, message: string | undefined) {
        if (message) this.errors[field] = message;
        else delete this.errors[field];
    }

    markTouched(field: string) {
        this.touched[field] = true;
    }

    get isValid() {
        return Object.keys(this.errors).length === 0;
    }
}

const key = Symbol('form');

export function provideForm() {
    const ctx = new FormContext();
    setContext(key, ctx);
    return ctx;
}

export function useForm(): FormContext {
    const ctx = getContext<FormContext | undefined>(key);
    if (!ctx) throw new Error('Field muss in einem Form-Kontext stehen');
    return ctx;
}
svelte Form.svelte
<script>
    import { provideForm } from '$lib/contexts/form.svelte';

    let { children, onsubmit } = $props();
    const form = provideForm();

    function handleSubmit(event) {
        event.preventDefault();
        if (form.isValid) onsubmit?.(event);
    }
</script>

<form onsubmit={handleSubmit}>
    {@render children()}
    <button type="submit" disabled={!form.isValid}>Senden</button>
</form>
svelte EmailField.svelte
<script>
    import { useForm } from '$lib/contexts/form.svelte';

    const form = useForm();
    let value = $state('');

    $effect(() => {
        form.setError(
            'email',
            value.includes('@') ? undefined : 'Ungültige E-Mail',
        );
    });
</script>

<label>
    E-Mail
    <input
        type="email"
        bind:value
        onblur={() => form.markTouched('email')}
    />
    {#if form.touched.email && form.errors.email}
        <small>{form.errors.email}</small>
    {/if}
</label>

<EmailField> weiß nichts von anderen Feldern und nichts vom Form-Element. Es kennt nur seinen useForm()-Helfer. Trotzdem funktioniert die zusammengesetzte Validierung — form.isValid deaktiviert den Submit-Button, sobald ein Feld einen Fehler meldet.

Häufige Stolperfallen

Reaktivität verlieren beim Destrukturieren.

ts Verliert Reaktivität
const theme = useTheme();
const { value } = theme; // einmaliger Snapshot

Das Property direkt zu lesen ist reaktiv, eine Destrukturierung kopiert den aktuellen Wert. In Komponenten immer theme.value schreiben, nicht zwischenspeichern.

Klasse ohne $state für die Properties. Eine class Theme { value = 'light' } ist nicht reaktiv. Erst value = $state('light') macht das Property reaktiv. Wer das vergisst, wundert sich, dass Updates im UI nicht ankommen.

Context vor setContext lesen. Wenn du in einer Komponente getContext aufrufst und keine Eltern-Komponente vorher setContext mit dem gleichen Schlüssel gemacht hat, bekommst du undefined. Eigene Wrapper-Funktionen mit Throw sind hier Gold wert.

Reaktivität über Komponenten-Grenze nicht reaktiv. Wenn du einen reaktiven Wert über getContext holst und ihn an eine Library weitergibst, die nichts von Sveltes Tracking weiß, kann die Library das Update nicht spüren. Lösung: An der Schnittstelle ein $effect einsetzen, das auf den Wert reagiert und die Library aktualisiert.

Eine Klasse per setContext ablegen, die Funktionen mit this enthält. Wenn du Methoden später als Callbacks weitergibst, kann der this-Bezug verloren gehen. Bind beim Übergeben (onclick={() => theme.toggle()} ist sicher; onclick={theme.toggle} kann wackeln).

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu Context API

Zur Übersicht