Listen rendern Svelte-Templates mit dem {#each}-Block. Er sieht klassischer aus als das React-map()-Pattern, ist aber kompakter und bietet eingebaute Features wie Keyed Iteration und einen {:else}-Fallback bei leeren Arrays. Dieser Artikel zeigt alle Varianten und erklärt, warum Keys bei dynamischen Listen entscheidend sind.
Grundsyntax
<script>
let users = $state([
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Bernd' },
{ id: 3, name: 'Carla' },
]);
</script>
<ul>
{#each users as user}
<li>{user.name}</li>
{/each}
</ul>{#each items as item} öffnet den Block, {/each} schließt ihn. Die einzelne Iteration rendert das Markup einmal pro Element.
Index zugreifen
Ein zweiter Parameter liefert den Index:
<ol>
{#each users as user, index}
<li>{index + 1}. {user.name}</li>
{/each}
</ol>Keyed Each Blocks
Bei dynamischen Listen — Hinzufügen, Entfernen oder Umsortieren — ist eine Identität pro Element wichtig. Sonst weiß Svelte beim Diffing nicht, welches alte Element zu welchem neuen gehört, und behandelt verschobene Elemente fälschlich als „verändert”.
<ul>
{#each users as user (user.id)}
<li>{user.name}</li>
{/each}
</ul>Der Ausdruck in Klammern ((user.id)) ist der Key. Er muss eindeutig pro Element sein und stabil zwischen Renders.
Warum Keys wichtig sind
Stell dir eine TodoList mit Checkbox-State pro Eintrag vor:
{#each todos as todo}
<li>
<input type="checkbox" />
{todo.text}
</li>
{/each}Wenn ein Eintrag oben entfernt wird, verschieben sich Index und DOM-Position. Ohne Key denkt Svelte: „Erster <li> hat sich nur im Text geändert.” Der Checkbox-State (lokales DOM-State) bleibt am alten Index hängen — und ist falsch zugeordnet.
{#each todos as todo (todo.id)}
<li>
<input type="checkbox" />
{todo.text}
</li>
{/each}Mit Key erkennt Svelte: „Eintrag mit ID 5 ist weg” — und zerstört genau diesen <li>-Eintrag. Der State der anderen bleibt korrekt erhalten.
{:else} – Fallback bei leerer Liste
Eingebauter Fallback für leere Arrays:
{#each todos as todo (todo.id)}
<li>{todo.text}</li>
{:else}
<li>Keine Aufgaben vorhanden.</li>
{/each}Spart das {#if todos.length}...{:else}...{/if}-Konstrukt um den {#each} herum.
Destrukturierung
Item-Felder lassen sich direkt destrukturieren:
{#each users as { id, name, role } (id)}
<li>
<strong>{name}</strong> ({role})
</li>
{/each}Auch Array-Destrukturierung funktioniert:
{#each entries as [key, value] (key)}
<dt>{key}</dt>
<dd>{value}</dd>
{/each}Über Iterables, nicht nur Arrays
{#each} iteriert über alles, was in einem for...of funktioniert: Arrays, Map, Set, Generatoren.
<script>
const settings = new Map([
['theme', 'dark'],
['locale', 'de'],
]);
</script>
<dl>
{#each settings as [key, value] (key)}
<dt>{key}</dt>
<dd>{value}</dd>
{/each}
</dl>Bei reaktiven Maps oder Sets aus svelte/reactivity (SvelteMap, SvelteSet) reagiert die Liste automatisch auf Änderungen.
Verschachtelte each-Blöcke
<table>
{#each rows as row (row.id)}
<tr>
{#each row.cells as cell, index (index)}
<td>{cell}</td>
{/each}
</tr>
{/each}
</table>Beachten: Bei verschachtelten Schleifen ist jeder Key nur innerhalb seines eigenen Blocks eindeutig. Du brauchst nicht App-weit eindeutige IDs.
Häufige Stolperfallen
Index als Key bei dynamischen Listen.
{#each items as item, i (i)}
...
{/each}Funktioniert nur bei statischen Listen. Bei Insert/Delete in der Mitte verhält sich das wie „kein Key” — Identität geht verloren.
Zufalls-Key zur Render-Zeit.
{#each items as item (Math.random())}Bei jedem Render bekommt jeder Eintrag einen neuen Key — Svelte unmounted und remounted alle Items. Teuer und zerstört State.
Mutationen ohne Reaktivität.
Bei nicht-reaktiven Arrays (kein $state) ändert sich die Liste zwar im Speicher, aber Svelte rendert nicht neu. Liste muss reaktiv sein.
Schreibzugriffe auf das Iterations-Item.
{#each items as item} — das item ist ein normaler Loop-Bezeichner, kein bind-bares Ziel. Wenn du das Item ändern willst, greife per Index oder ID auf das Original-Array zu.
Vergessenes {/each}.
Compiler-Fehler. Wie bei {#if} müssen Blöcke explizit geschlossen werden.
Praxis-Beispiel: Filter-Tabelle
<script>
let users = $state([...]);
let query = $state('');
let filtered = $derived(
users.filter((u) =>
u.name.toLowerCase().includes(query.toLowerCase())
)
);
</script>
<input bind:value={query} placeholder="Suchen..." />
<ul>
{#each filtered as user (user.id)}
<li>{user.name}</li>
{:else}
<li>Keine Treffer.</li>
{/each}
</ul>filtered ist ein $derived — bei jeder Eingabe neu berechnet, der {#each} reagiert sofort.