Declarative List Rendering
At the heart of every dynamic web application lies the need to render collections of data—user lists, product catalogs, notification feeds, table rows, navigation items. While JavaScript offers Array.prototype.map() for transforming arrays, Svelte 5’s #each block provides something far more powerful: a declarative, reactive, and highly optimized mechanism for rendering lists that automatically updates when your data changes.
Understanding #each deeply isn’t just about learning syntax—it’s about understanding how Svelte tracks DOM elements, why keys matter for performance and correctness, and how to leverage destructuring and patterns to write cleaner, more maintainable code. This tutorial explores every facet of list rendering in Svelte 5, from basic iteration to advanced patterns that will make your applications both performant and elegant.
The Foundation
Basic Each Block Syntax
The #each block iterates over any value that can be used with Array.from()—arrays, array-like objects (anything with a length property), or iterables like Map and Set.
{#each expression as name}
<!-- rendered for each item -->
{/each} Here’s the simplest example:
<script>
let fruits = $state(['Apple', 'Banana', 'Cherry', 'Date'])
</script>
<ul>
{#each fruits as fruit}
<li>{fruit}</li>
{/each}
</ul> This renders an unordered list with four items. When fruits changes—items added, removed, or modified—Svelte automatically updates the DOM to reflect the new state.
Handling null and undefined
If the expression evaluates to null or undefined, Svelte treats it as an empty array. This means no items render, and any {:else} block (covered later) will appear:
<script>
let items = $state(null)
async function loadItems() {
const response = await fetch('/api/items')
items = await response.json()
}
</script>
{#each items as item}
<div>{item.name}</div>
{:else}
<p>No items to display</p>
{/each}
<!-- Before loadItems() completes, "No items to display" shows --> This behavior eliminates the need for explicit null checks in your templates.
Accessing the Index
Often you need to know an item’s position within the list. The #each block provides an optional second parameter—the index:
{#each expression as name, index}
<!-- index is 0-based -->
{/each} <script>
let steps = $state(['Create account', 'Verify email', 'Complete profile', 'Start exploring'])
</script>
<ol class="setup-wizard">
{#each steps as step, i}
<li class="step">
<span class="step-number">{i + 1}</span>
<span class="step-label">{step}</span>
</li>
{/each}
</ol> The index is equivalent to the second argument in array.map((item, index) => ...). It’s zero-based and automatically updates when items are reordered, added, or removed.
Using Index for Conditional Styling
<script>
let tableData = $state([
{ name: 'Alice', role: 'Developer', department: 'Engineering' },
{ name: 'Bob', role: 'Designer', department: 'Product' },
{ name: 'Charlie', role: 'Manager', department: 'Operations' },
{ name: 'Diana', role: 'Analyst', department: 'Finance' }
])
</script>
<table>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Role</th>
<th>Department</th>
</tr>
</thead>
<tbody>
{#each tableData as row, index}
<tr class={{ even: index % 2 === 0, odd: index % 2 === 1 }}>
<td>{index + 1}</td>
<td>{row.name}</td>
<td>{row.role}</td>
<td>{row.department}</td>
</tr>
{/each}
</tbody>
</table>
<style>
.even {
background: #f8fafc;
}
.odd {
background: #ffffff;
}
</style> First and Last Item Detection
<script>
let breadcrumbs = $state([
{ label: 'Home', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Electronics', href: '/products/electronics' },
{ label: 'Laptops', href: '/products/electronics/laptops' }
])
</script>
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
{#each breadcrumbs as crumb, i}
{@const isFirst = i === 0}
{@const isLast = i === breadcrumbs.length - 1}
<li class={{ first: isFirst, current: isLast }}>
{#if isLast}
<span aria-current="page">{crumb.label}</span>
{:else}
<a href={crumb.href}>{crumb.label}</a>
<span class="separator">/</span>
{/if}
</li>
{/each}
</ol>
</nav> Keyed Each Blocks
This is where #each becomes truly powerful—and where many developers encounter their first subtle bugs.
{#each expression as name (key)}
<!-- key uniquely identifies each item -->
{/each} Why Keys Matter
Without a key, Svelte updates the list by index. When items change, Svelte compares the old and new arrays position by position:
- Position 0: Update if different
- Position 1: Update if different
- And so on…
This works fine for simple cases, but breaks down when items are reordered, inserted in the middle, or removed from anywhere except the end.
The Problem Without Keys:
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false },
{ id: 2, text: 'Build an app', done: false },
{ id: 3, text: 'Deploy', done: false }
])
function removeFirst() {
todos = todos.slice(1)
}
</script>
<!-- WITHOUT key: BUGGY -->
{#each todos as todo}
<div class="todo">
<input type="checkbox" bind:checked={todo.done} />
<span>{todo.text}</span>
</div>
{/each}
<button onclick={removeFirst}>Remove First</button> If you check the first checkbox and then click “Remove First,” you might expect the first todo to disappear. But without a key, Svelte updates by index:
- Position 0 now has “Build an app” → updates text, keeps checkbox state
- Position 1 now has “Deploy” → updates text, keeps checkbox state
- Position 2 no longer exists → removes last DOM element
The result: The “Build an app” item now appears checked, which is wrong!
The Solution With Keys:
{#each todos as todo (todo.id)}
<div class="todo">
<input type="checkbox" bind:checked={todo.done} />
<span>{todo.text}</span>
</div>
{/each} Now Svelte tracks each DOM element by its todo.id. When you remove the first todo:
- Svelte sees that
id: 1is gone → removes that specific DOM element - Items with
id: 2andid: 3remain untouched
The checkbox state stays with the correct item.
What Makes a Good Key?
Keys must uniquely identify each item within the list. Good keys are:
- Unique: No two items should have the same key
- Stable: The same item should always have the same key across renders
- Simple: Strings and numbers are preferred (they persist identity even when the object reference changes)
<!-- PREFERRED: Using unique database IDs -->
{#each users as user (user.id)}
<!-- PREFERRED: Using unique slugs or codes -->
{#each products as product (product.sku)}
<!-- PREFERRED: Compound keys when needed -->
{#each orderItems as item (`${item.orderId}-${item.productId}`)}
<!-- RISKY: Using index as key (same as no key for reordering) -->
{#each items as item, i (i)}
<!-- AVOID: Using non-unique values -->
{#each users as user (user.role)} <!-- Multiple users can share a role! -->
<!-- AVOID: Using object references that change -->
{#each items as item (item)} <!-- New object reference = new key --> Keys with Index
You can use both a key and index together:
{#each items as item, index (item.id)}
<div>
{index + 1}. {item.name}
</div>
{/each} The key tracks the DOM element identity, while the index reflects the current position.
When Keys Are Essential
Always use keys when:
- Items have internal state (form inputs, component state, animations)
- Items can be reordered (drag-and-drop, sorting)
- Items can be inserted or removed from the middle
- Using transitions or animations (Svelte needs to know which elements to animate)
- Components maintain their own state
<script>
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
let items = $state([
{ id: 1, name: 'Item A' },
{ id: 2, name: 'Item B' },
{ id: 3, name: 'Item C' }
])
function shuffle() {
items = items.sort(() => Math.random() - 0.5)
}
</script>
<button onclick={shuffle}>Shuffle</button>
<ul>
{#each items as item (item.id)}
<li animate:flip={{ duration: 300 }} transition:fade>
{item.name}
</li>
{/each}
</ul> Without the key, the FLIP animation wouldn’t work correctly—Svelte wouldn’t know which element moved where.
Cleaner Access to Item Properties
Svelte’s #each fully supports JavaScript destructuring patterns, allowing you to extract properties directly in the loop declaration.
Object Destructuring
Problem: Accessing properties with user.property in each block can be verbose and repetitive, especially with many properties.
Solution: Use object destructuring in the #each declaration to access properties directly and make your markup cleaner.
<script>
let users = $state([
{ id: 1, name: 'Alice Chen', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'User' },
{ id: 3, name: 'Carol Davis', email: 'carol@example.com', role: 'Moderator' }
])
</script>
<!-- Without destructuring -->
{#each users as user (user.id)}
<div class="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span class="role">{user.role}</span>
</div>
{/each}
<!-- With destructuring -->
{#each users as { id, name, email, role } (id)}
<div class="user-card">
<h3>{name}</h3>
<p>{email}</p>
<span class="role">{role}</span>
</div>
{/each} Notice that when destructuring, you can still reference destructured properties in the key expression.
Nested Destructuring
Problem: Accessing deeply nested properties (like order.customer.address.city) in each block can clutter your markup and make it harder to read.
Solution: Use nested destructuring in the #each declaration to pull out nested values directly.
<script>
let orders = $state([
{
id: 'ORD-001',
customer: { name: 'Alice', address: { city: 'Seattle', zip: '98101' } },
total: 149.99
},
{
id: 'ORD-002',
customer: { name: 'Bob', address: { city: 'Portland', zip: '97201' } },
total: 89.5
}
])
</script>
{#each orders as { id, customer: { name, address: { city } }, total } (id)}
<div class="order-row">
<span class="order-id">{id}</span>
<span class="customer">{name}</span>
<span class="city">{city}</span>
<span class="total">${total.toFixed(2)}</span>
</div>
{/each} Destructuring with Defaults
Problem: Some items in your array may have missing or optional properties, which can lead to undefined values in your markup.
Solution: Use default values in destructuring to provide fallbacks for missing properties.
<script>
let notifications = $state([
{ id: 1, message: 'Welcome!', type: 'info' },
{ id: 2, message: 'Error occurred' }, // type is missing
{ id: 3, message: 'Success!', type: 'success' }
])
</script>
{#each notifications as { id, message, type = 'default' } (id)}
<div class="notification notification-{type}">
{message}
</div>
{/each} Destructuring with Index
Problem: You want to access both item properties and the current index in the loop, but referencing both can be verbose.
Solution: Destructure both the item and the index in the #each declaration for concise access.
{#each items as { id, name, value }, index (id)}
<div class="item">
<span class="position">#{index + 1}</span>
<span class="name">{name}</span>
<span class="value">{value}</span>
</div>
{/each} Else Blocks
Handling Empty Lists
The {:else} clause renders when the list is empty (or null/undefined):
{#each expression as name}
<!-- items -->
{:else}
<!-- shown when list is empty -->
{/each} <script>
let searchResults = $state([])
let searchQuery = $state('')
let hasSearched = $state(false)
async function search() {
hasSearched = true
const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`)
searchResults = await response.json()
}
</script>
<form
onsubmit={(e) => {
e.preventDefault()
search()
}}
>
<input bind:value={searchQuery} placeholder="Search..." />
<button type="submit">Search</button>
</form>
{#each searchResults as result (result.id)}
<div class="result-item">
<h3>{result.title}</h3>
<p>{result.description}</p>
</div>
{:else}
{#if hasSearched}
<div class="no-results">
<p>No results found for "{searchQuery}"</p>
<p>Try different keywords or check your spelling.</p>
</div>
{:else}
<div class="search-prompt">
<p>Enter a search term to find results.</p>
</div>
{/if}
{/each} Empty State Components
Problem: You want to show a friendly, reusable empty state UI when a list is empty, instead of just plain text.
Solution: Use the {:else} block to render a custom empty state component when the array is empty.
<script>
import EmptyState from './EmptyState.svelte'
let notifications = $state([])
</script>
<div class="notifications-panel">
<h2>Notifications</h2>
{#each notifications as notification (notification.id)}
<div class="notification notification-{notification.type}">
<span class="icon">{notification.icon}</span>
<p>{notification.message}</p>
<time>{notification.timestamp}</time>
</div>
{:else}
<EmptyState
icon="🔔"
title="No notifications"
message="You're all caught up! Check back later for updates."
/>
{/each}
</div> Iterating Over Different Data Structures
Maps
Problem: You want to iterate over a JavaScript Map and render both the key and value for each entry.
Solution: Use array destructuring in the #each block to access both key and value directly.
<script>
let userRoles = $state(
new Map([
['alice', 'Admin'],
['bob', 'Editor'],
['carol', 'Viewer']
])
)
</script>
<ul>
{#each userRoles as [username, role] (username)}
<li>
<strong>{username}</strong>: {role}
</li>
{/each}
</ul> Sets
Problem: You want to render a list of unique tags from a Set and allow users to remove them interactively.
Solution: Use a #each block to iterate over the Set, and update the Set reactively when a tag is removed.
<script>
let tags = $state(new Set(['svelte', 'javascript', 'frontend', 'web']))
function removeTag(tag) {
tags.delete(tag)
tags = new Set(tags) // Trigger reactivity
}
</script>
<div class="tag-list">
{#each tags as tag (tag)}
<span class="tag">
{tag}
<button onclick={() => removeTag(tag)}>×</button>
</span>
{/each}
</div> Object Entries
Problem: You want to render a table of key-value pairs from an object, allowing editing of each value, including booleans.
Solution: Use Object.entries() with a #each block to iterate over the object’s properties, and render appropriate input controls for each value type.
<script>
let config = $state({
theme: 'dark',
language: 'en',
notifications: true,
autoSave: false
})
</script>
<table class="config-table">
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{#each Object.entries(config) as [key, value] (key)}
<tr>
<td>{key}</td>
<td>
{#if typeof value === 'boolean'}
<input
type="checkbox"
checked={value}
onchange={(e) => (config[key] = e.target.checked)}
/>
{:else}
<input type="text" {value} oninput={(e) => (config[key] = e.target.value)} />
{/if}
</td>
</tr>
{/each}
</tbody>
</table> Generator Functions
Problem: You want to render a list of numbers generated by a generator function, such as the Fibonacci sequence, and display them in the UI.
Solution: Use a generator to produce the sequence, convert it to an array, and iterate with a #each block.
<script>
function* fibonacci(limit) {
let a = 0,
b = 1
while (a <= limit) {
yield a
;[a, b] = [b, a + b]
}
}
let maxValue = $state(100)
let fibNumbers = $derived([...fibonacci(maxValue)])
</script>
<input type="range" bind:value={maxValue} min="10" max="1000" />
<p>Fibonacci numbers up to {maxValue}:</p>
<div class="number-grid">
{#each fibNumbers as num, i (i)}
<span class="fib-number">{num}</span>
{/each}
</div> Nested Each Blocks
Complex data structures often require nested iterations:
<script>
let departments = $state([
{
name: 'Engineering',
teams: [
{ name: 'Frontend', members: ['Alice', 'Bob'] },
{ name: 'Backend', members: ['Carol', 'Dave', 'Eve'] },
{ name: 'DevOps', members: ['Frank'] }
]
},
{
name: 'Design',
teams: [
{ name: 'UI/UX', members: ['Grace', 'Henry'] },
{ name: 'Brand', members: ['Ivy'] }
]
}
])
</script>
<div class="org-chart">
{#each departments as dept (dept.name)}
<section class="department">
<h2>{dept.name}</h2>
{#each dept.teams as team (team.name)}
<div class="team">
<h3>{team.name}</h3>
<ul class="members">
{#each team.members as member (member)}
<li>{member}</li>
{/each}
</ul>
</div>
{/each}
</section>
{/each}
</div> Flattening with Index Tracking
Sometimes you need to maintain awareness of multiple nesting levels:
<script>
let categories = $state([
{
name: 'Electronics',
products: [
{ id: 'e1', name: 'Laptop', price: 999 },
{ id: 'e2', name: 'Phone', price: 699 }
]
},
{
name: 'Clothing',
products: [
{ id: 'c1', name: 'Jacket', price: 149 },
{ id: 'c2', name: 'Shoes', price: 89 },
{ id: 'c3', name: 'Hat', price: 29 }
]
}
])
</script>
<table>
<thead>
<tr>
<th>Category</th>
<th>#</th>
<th>Product</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{#each categories as category, categoryIndex (category.name)}
{#each category.products as product, productIndex (product.id)}
<tr>
{#if productIndex === 0}
<td rowspan={category.products.length} class="category-cell">
{category.name}
</td>
{/if}
<td>{productIndex + 1}</td>
<td>{product.name}</td>
<td>${product.price}</td>
</tr>
{/each}
{/each}
</tbody>
</table> Reactivity and Each Blocks
Understanding how #each interacts with Svelte 5’s reactivity system is crucial for building performant applications.
Reactive Arrays with $state
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false },
{ id: 2, text: 'Build something', done: false }
])
let newTodoText = $state('')
function addTodo() {
if (!newTodoText.trim()) return
todos.push({
id: Date.now(),
text: newTodoText,
done: false
})
newTodoText = ''
}
function removeTodo(id) {
const index = todos.findIndex((t) => t.id === id)
if (index !== -1) {
todos.splice(index, 1)
}
}
function toggleTodo(id) {
const todo = todos.find((t) => t.id === id)
if (todo) {
todo.done = !todo.done
}
}
</script>
<form
onsubmit={(e) => {
e.preventDefault()
addTodo()
}}
>
<input bind:value={newTodoText} placeholder="New todo..." />
<button type="submit">Add</button>
</form>
<ul>
{#each todos as todo (todo.id)}
<li class={{ done: todo.done }}>
<input type="checkbox" checked={todo.done} onchange={() => toggleTodo(todo.id)} />
<span>{todo.text}</span>
<button onclick={() => removeTodo(todo.id)}>Delete</button>
</li>
{/each}
</ul>
<style>
.done span {
text-decoration: line-through;
opacity: 0.6;
}
</style> In Svelte 5, array mutations like push, splice, and property assignments are tracked automatically when the array is declared with $state.
Derived Lists with $derived
<script>
let products = $state([
{ id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },
{ id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },
{ id: 3, name: 'Notebook', price: 5, category: 'office', inStock: false },
{ id: 4, name: 'Keyboard', price: 79, category: 'electronics', inStock: true },
{ id: 5, name: 'Pen', price: 2, category: 'office', inStock: true }
])
let categoryFilter = $state('all')
let showInStockOnly = $state(false)
let sortBy = $state('name')
let sortOrder = $state('asc')
let filteredProducts = $derived.by(() => {
let result = products
// Filter by category
if (categoryFilter !== 'all') {
result = result.filter((p) => p.category === categoryFilter)
}
// Filter by stock
if (showInStockOnly) {
result = result.filter((p) => p.inStock)
}
// Sort
result = [...result].sort((a, b) => {
let aVal = a[sortBy]
let bVal = b[sortBy]
if (typeof aVal === 'string') {
return sortOrder === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
}
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal
})
return result
})
let categories = $derived([...new Set(products.map((p) => p.category))])
</script>
<div class="filters">
<select bind:value={categoryFilter}>
<option value="all">All Categories</option>
{#each categories as category (category)}
<option value={category}>{category}</option>
{/each}
</select>
<label>
<input type="checkbox" bind:checked={showInStockOnly} />
In stock only
</label>
<select bind:value={sortBy}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
<button onclick={() => (sortOrder = sortOrder === 'asc' ? 'desc' : 'asc')}>
{sortOrder === 'asc' ? '↑' : '↓'}
</button>
</div>
<p>{filteredProducts.length} of {products.length} products</p>
<div class="product-grid">
{#each filteredProducts as product (product.id)}
<div class={['product-card', !product.inStock && 'out-of-stock']}>
<h3>{product.name}</h3>
<p class="price">${product.price}</p>
<span class="category">{product.category}</span>
{#if !product.inStock}
<span class="badge">Out of Stock</span>
{/if}
</div>
{:else}
<p class="no-results">No products match your filters.</p>
{/each}
</div> Animations and Transitions
The #each block integrates beautifully with Svelte’s animation system.
Entry and Exit Transitions
<script>
import { fade, fly, slide } from 'svelte/transition'
let items = $state([
{ id: 1, text: 'First item' },
{ id: 2, text: 'Second item' },
{ id: 3, text: 'Third item' }
])
function addItem() {
items.push({
id: Date.now(),
text: `Item ${items.length + 1}`
})
}
function removeItem(id) {
const index = items.findIndex((i) => i.id === id)
if (index !== -1) items.splice(index, 1)
}
</script>
<button onclick={addItem}>Add Item</button>
<ul>
{#each items as item (item.id)}
<li transition:slide={{ duration: 300 }}>
<span>{item.text}</span>
<button onclick={() => removeItem(item.id)}>Remove</button>
</li>
{/each}
</ul> FLIP Animations for Reordering
The animate:flip directive creates smooth animations when list items change position:
<script>
import { flip } from 'svelte/animate'
import { quintOut } from 'svelte/easing'
let items = $state([
{ id: 1, name: 'Apple', votes: 0 },
{ id: 2, name: 'Banana', votes: 0 },
{ id: 3, name: 'Cherry', votes: 0 },
{ id: 4, name: 'Date', votes: 0 }
])
// Sort by votes (highest first) whenever votes change
let sortedItems = $derived([...items].sort((a, b) => b.votes - a.votes))
function vote(id) {
const item = items.find((i) => i.id === id)
if (item) item.votes++
}
</script>
<p>Click to vote! Items reorder by popularity.</p>
<ul class="voting-list">
{#each sortedItems as item (item.id)}
<li animate:flip={{ duration: 400, easing: quintOut }}>
<button onclick={() => vote(item.id)}>
<span class="name">{item.name}</span>
<span class="votes">{item.votes} votes</span>
</button>
</li>
{/each}
</ul>
<style>
.voting-list {
list-style: none;
padding: 0;
}
.voting-list li {
margin: 0.5rem 0;
}
.voting-list button {
width: 100%;
padding: 1rem;
display: flex;
justify-content: space-between;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.voting-list button:hover {
background: #e2e8f0;
}
</style> Combining Transitions and Animations
<script>
import { flip } from 'svelte/animate'
import { fade, scale } from 'svelte/transition'
let items = $state([
{ id: 1, text: 'Item A' },
{ id: 2, text: 'Item B' },
{ id: 3, text: 'Item C' }
])
function shuffle() {
items = items.sort(() => Math.random() - 0.5)
}
function addItem() {
items.push({ id: Date.now(), text: `Item ${items.length + 1}` })
}
function removeItem(id) {
const index = items.findIndex((i) => i.id === id)
if (index !== -1) items.splice(index, 1)
}
</script>
<div class="controls">
<button onclick={addItem}>Add</button>
<button onclick={shuffle}>Shuffle</button>
</div>
<ul>
{#each items as item (item.id)}
<li
animate:flip={{ duration: 300 }}
in:scale={{ duration: 200, start: 0.8 }}
out:fade={{ duration: 150 }}
>
<span>{item.text}</span>
<button onclick={() => removeItem(item.id)}>×</button>
</li>
{/each}
</ul> Common Struggles and Solutions
1: Items Not Updating Correctly
Problem: You modify an item’s property, but the UI doesn’t update.
<script>
// AVOID: This might not update correctly in older patterns
let items = [{ id: 1, count: 0 }]
function increment(id) {
const item = items.find((i) => i.id === id)
item.count++ // Direct mutation
}
</script> Solution: Use $state for automatic reactivity:
<script>
// PREFERRED: With $state, mutations are tracked
let items = $state([{ id: 1, count: 0 }])
function increment(id) {
const item = items.find((i) => i.id === id)
if (item) item.count++
}
</script> 2: Keying Issues with Objects
Problem: Using the object itself as a key causes unnecessary re-renders.
<!-- AVOID: Using object - references change, causing all items to re-render -->
{#each items as item (item)}
<Component data={item} />
{/each} Solution: Use a stable, unique identifier:
<!-- PREFERRED: Stable key -->
{#each items as item (item.id)}
<Component data={item} />
{/each} 3: Losing Component State on Reorder
Problem: Drag-and-drop reordering resets component internal state.
Cause: Missing or incorrect keys mean Svelte recreates components instead of moving them.
Solution: Always use unique, stable keys with stateful child components:
<script>
import EditableCard from './EditableCard.svelte'
let cards = $state([
{ id: 'card-1', title: 'First' },
{ id: 'card-2', title: 'Second' },
{ id: 'card-3', title: 'Third' }
])
</script>
{#each cards as card (card.id)}
<!-- EditableCard maintains its own editing state -->
<!-- The key ensures it's preserved across reorders -->
<EditableCard data={card} />
{/each} 4: Performance with Large Lists
Problem: Rendering thousands of items causes sluggish UI.
Solution 1: Virtual scrolling (render only visible items):
<script>
let items = $state(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i + 1}`
}))
)
let containerHeight = 400
let itemHeight = 40
let scrollTop = $state(0)
let visibleItems = $derived.by(() => {
const startIndex = Math.floor(scrollTop / itemHeight)
const visibleCount = Math.ceil(containerHeight / itemHeight) + 1
const endIndex = Math.min(startIndex + visibleCount, items.length)
return items.slice(startIndex, endIndex).map((item, i) => ({
...item,
offset: (startIndex + i) * itemHeight
}))
})
let totalHeight = $derived(items.length * itemHeight)
</script>
<div
class="virtual-list"
style="height: {containerHeight}px"
onscroll={(e) => (scrollTop = e.target.scrollTop)}
>
<div class="spacer" style="height: {totalHeight}px">
{#each visibleItems as item (item.id)}
<div class="item" style="position: absolute; top: {item.offset}px; height: {itemHeight}px">
{item.text}
</div>
{/each}
</div>
</div>
<style>
.virtual-list {
overflow-y: auto;
position: relative;
}
.spacer {
position: relative;
}
.item {
width: 100%;
display: flex;
align-items: center;
padding: 0 1rem;
box-sizing: border-box;
}
</style> Solution 2: Pagination:
<script>
let allItems = $state([
/* large array */
])
let page = $state(1)
let pageSize = 20
let paginatedItems = $derived(allItems.slice((page - 1) * pageSize, page * pageSize))
let totalPages = $derived(Math.ceil(allItems.length / pageSize))
</script>
{#each paginatedItems as item (item.id)}
<div>{item.name}</div>
{/each}
<div class="pagination">
<button disabled={page === 1} onclick={() => page--}>Previous</button>
<span>Page {page} of {totalPages}</span>
<button disabled={page === totalPages} onclick={() => page++}>Next</button>
</div> 5: Duplicate Keys Warning
Problem: Console shows “duplicate key” warnings.
Cause: Two or more items have the same key value.
Solution: Ensure keys are truly unique, or create compound keys:
<script>
// If items from different sources might have same IDs:
let itemsFromSourceA = $state([{ id: 1, name: 'A1' }])
let itemsFromSourceB = $state([{ id: 1, name: 'B1' }]) // Same ID!
let allItems = $derived([
...itemsFromSourceA.map((i) => ({ ...i, source: 'A' })),
...itemsFromSourceB.map((i) => ({ ...i, source: 'B' }))
])
</script>
<!-- Use compound key -->
{#each allItems as item (`${item.source}-${item.id}`)}
<div>{item.name}</div>
{/each} 6: Async Data in Lists
Problem: Each item needs to fetch additional data.
Solution: Use {#await} inside #each:
<script>
let userIds = $state([1, 2, 3, 4, 5])
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
</script>
<ul>
{#each userIds as userId (userId)}
<li>
{#await fetchUser(userId)}
<span class="loading">Loading user {userId}...</span>
{:then user}
<span>{user.name} - {user.email}</span>
{:catch error}
<span class="error">Failed to load user {userId}</span>
{/await}
</li>
{/each}
</ul> Best Practices Summary
Always use keys for lists with stateful elements, reordering, or animations. The only exception is truly static, never-changing lists.
Use stable, unique identifiers as keys—database IDs, UUIDs, or unique slugs. Avoid using array indices except for truly static data.
Leverage destructuring to make templates cleaner and more readable. Extract only what you need.
Use
$derivedfor filtered, sorted, or transformed lists. Keep the transformation logic in the script section, not the template.Combine with
{:else}to handle empty states gracefully. Never let users see a blank screen.Use
{@const}inside each blocks for computed values that are per-item specific.Consider performance for large lists. Implement virtual scrolling or pagination when dealing with hundreds or thousands of items.
Test reordering behavior when using stateful child components. Ensure keys are set correctly.
Use FLIP animations (
animate:flip) for smooth reordering effects.Handle async data appropriately—either fetch all data before rendering, or use
{#await}per item with proper loading states.
Quick Reference
<!-- Basic iteration -->
{#each items as item}
<div>{item.name}</div>
{/each}
<!-- With index -->
{#each items as item, index}
<div>{index + 1}. {item.name}</div>
{/each}
<!-- With key (recommended for dynamic lists) -->
{#each items as item (item.id)}
<div>{item.name}</div>
{/each}
<!-- Key with index -->
{#each items as item, index (item.id)}
<div>{index}: {item.name}</div>
{/each}
<!-- Object destructuring -->
{#each items as { id, name, value } (id)}
<div>{name}: {value}</div>
{/each}
<!-- Array destructuring -->
{#each coordinates as [x, y] (x + '-' + y)}
<div>({x}, {y})</div>
{/each}
<!-- Rest pattern -->
{#each items as { id, ...rest } (id)}
<Component {...rest} />
{/each}
<!-- Without item (repeat N times) -->
{#each { length: 5 } as, i}
<div>Item {i + 1}</div>
{/each}
<!-- With else block -->
{#each items as item (item.id)}
<div>{item.name}</div>
{:else}
<p>No items found.</p>
{/each}
<!-- With animations -->
{#each items as item (item.id)}
<div animate:flip={{ duration: 300 }} transition:fade>
{item.name}
</div>
{/each}
<!-- Nested each blocks -->
{#each categories as category (category.id)}
<section>
<h2>{category.name}</h2>
{#each category.items as item (item.id)}
<div>{item.name}</div>
{/each}
</section>
{/each} Conclusion
The #each block is deceptively simple in its basic form, yet mastering its nuances—keys, destructuring, reactivity, animations—unlocks the ability to build sophisticated, performant, and maintainable user interfaces. Understanding why keys matter, when to use derived state, and how to handle edge cases will serve you well as you build increasingly complex Svelte applications. This fundamental building block transforms raw data arrays into interactive, animated, and reactive UI components with remarkable efficiency.
The true power of #each emerges when combined with Svelte’s ecosystem: $derived for list transformations, @const for per-item calculations, bind: for form arrays, and animate: for smooth reordering. By understanding the reactivity model—that #each responds to array reassignment, not mutation—and implementing proper keying strategies, you can build list interfaces that are both performant and maintainable. Whether rendering simple lists or complex nested hierarchies, #each provides the foundation for data-driven interfaces.
Key Takeaways
#eachblocks iterate arrays reactively, responding to both direct mutations (likepush,splice, property assignments) and reassignments when declared with$state— Svelte 5’s proxy-based reactivity tracks all changes automatically- Key expressions are critical for performance - use unique identifiers like
(item.id)to enable efficient DOM diffing and prevent state mixups during reordering - Destructuring simplifies item access with
#each items as { id, name, price }extracting properties directly, andas item, indexproviding zero-based positional indices {:else}blocks handle empty arrays elegantly, rendering fallback UI whenitems.length === 0without separate conditional logic- Index parameter is zero-based and reactive - updates automatically when items are added/removed, but avoid using as keys for items that can be reordered
- Block-scoped
@constenables per-item calculations like{@const total = price * quantity}without polluting the component script with presentation logic - Animations require the
animate:directive with key expressions -animate:flipsmoothly repositions items during reordering, requiring unique keys to track movement - Nested
#eachblocks support hierarchical data like trees or grouped lists, with each level maintaining independent iteration context and key spaces
See Also
- Official Svelte 5 Documentation -
{#each} - Svelte Animations -
animate:- Theflipanimation for smooth reordering $derived- Reactive list transformations (filtering, sorting, mapping)- Svelte Transitions - Entry/exit animations for list items
- MDN - Array Methods - JavaScript array manipulation techniques