Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 080 / 100

each blocks with keys for Svelte & SvelteKit developers

Render lists correctly.

Expona Admin7 min read

We've all seen list rendering go wrong in production: items disappear, animations stutter, or state gets scrambled when the data changes. The culprit is usually missing or incorrect keys in Svelte's each blocks. Understanding each blocks with keys for Svelte & SvelteKit developers is the difference between a smooth UI and a frustrating one.

Why keys matter in each blocks

When Svelte renders a list with an each block, it needs to track which DOM node corresponds to which data item. Without a key, Svelte uses the array index by default—but that's fragile. If you reorder, insert, or delete items, Svelte may update the wrong node or lose component state entirely.

A key tells Svelte: "this specific data item maps to this specific DOM element." It's a contract that persists across renders. In production systems—whether we're building e-commerce carts, ride-hailing pickup lists, or SaaS dashboards—getting keys right prevents subtle bugs that are hard to debug.

Here's the minimal example:

typescript
<!-- Without keys (risky) -->
{#each items as item}
  <div>{item.name}</div>
{/each}

<!-- With keys (correct) -->
{#each items as item (item.id)}
  <div>{item.name}</div>
{/each}

The key—in this case item.id—is wrapped in parentheses after the item name. It should be a value that uniquely identifies each item and doesn't change for the lifetime of that item.

Using (item.id) keys to maintain identity

The most reliable key is a stable identifier like a database ID. In any real application, you're fetching data from a database or API, and each item already has an ID. Use that.

Let's say we're building a product listing for an e-commerce platform. Each product has a SKU that never changes:

typescript
let products = [
  { sku: 'prod-001', name: 'Widget', price: 29.99 },
  { sku: 'prod-002', name: 'Gadget', price: 49.99 },
  { sku: 'prod-003', name: 'Doohickey', price: 19.99 }
];

{#each products as product (product.sku)}
  <div class="product-card">
    <h3>{product.name}</h3>
    <p>${product.price}</p>
    <button on:click={() => addToCart(product)}>Add</button>
  </div>
{/each}

Now if we reorder products, remove one, or update prices, Svelte knows exactly which DOM element belongs to which product. Component state (like a quantity input inside a custom component) stays attached to the right product.

In a trading platform where we're displaying a live order book, you'd use the order ID. In a ride-hailing app, the trip ID. The principle is the same: pick a stable, unique identifier.

Animations need keys to work correctly

This is where missing keys cause visible, frustrating bugs. Svelte's transition: and animate: directives only work properly when Svelte knows which element is which across renders.

Imagine we're building a CRM and displaying a list of contacts. We want a slide transition when items enter or leave:

typescript
<script>
  import { slide } from 'svelte/transition';
  
  let contacts = [
    { id: 'c1', name: 'Alice', email: 'alice@example.com' },
    { id: 'c2', name: 'Bob', email: 'bob@example.com' }
  ];

  function removeContact(id) {
    contacts = contacts.filter(c => c.id !== id);
  }
</script>

{#each contacts as contact (contact.id)}
  <div transition:slide>
    <p>{contact.name} - {contact.email}</p>
    <button on:click={() => removeContact(contact.id)}>Remove</button>
  </div>
{/each}

Without the (contact.id) key, when you remove Bob, Svelte might animate out Alice's row instead (it doesn't know which is which). With the key, the slide animation plays on the correct element.

The same applies to animate:flip, which animates reordering. Without keys, you get janky, confusing behavior. With keys, Svelte smoothly animates items moving from one position to another. This is especially noticeable in real-time dashboards or shopping carts where data changes frequently.

Index keys break when you reorder

Using the array index as a key—or omitting the key entirely and letting Svelte default to the index—seems convenient but causes problems the moment your list changes order.

Here's a concrete scenario from a SaaS time-tracking app. We display tasks in priority order:

typescript
<script>
  let tasks = [
    { id: 't1', title: 'Fix bug', priority: 1, completed: false },
    { id: 't2', title: 'Write docs', priority: 2, completed: false },
    { id: 't3', title: 'Deploy', priority: 3, completed: false }
  ];

  function markDone(taskId) {
    tasks = tasks.map(t => 
      t.id === taskId ? { ...t, completed: true } : t
    );
  }

  function reSortByPriority() {
    // User changes priorities in the UI
    // Array order changes
  }
</script>

<!-- WRONG: using index -->
{#each tasks as task, i (i)}
  <div>
    <input type="checkbox" checked={task.completed} />
    <span>{task.title}</span>
  </div>
{/each}

<!-- CORRECT: using stable ID -->
{#each tasks as task (task.id)}
  <div>
    <input type="checkbox" checked={task.completed} />
    <span>{task.title}</span>
  </div>
{/each}

Imagine the tasks reorder. With index-based keys, the checkbox state might get attached to the wrong task because Svelte thinks "the element at index 0 should now show the second task." With ID-based keys, Svelte knows that the checkbox belongs to task t1 regardless of its position.

We've seen this bug ship to production in several forms: checkboxes end up on the wrong item, form input values drift between rows, or a user clicks "delete" on task A and task B vanishes instead. Always use a stable identifier, never the index.

Using {:else} for empty lists

While we're discussing each blocks, it's worth covering the :else clause. It renders when the list is empty—a common pattern for better UX.

Here's a typical flow from a travel booking app, showing available flights:

typescript
<script>
  let flights = [];
  let loading = false;
  let error = null;

  async function searchFlights(from, to, date) {
    loading = true;
    error = null;
    try {
      const res = await fetch(`/api/flights?from=${from}&to=${to}&date=${date}`);
      flights = await res.json();
    } catch (e) {
      error = e.message;
      flights = [];
    } finally {
      loading = false;
    }
  }
</script>

{#if loading}
  <p>Searching for flights...</p>
{:else if error}
  <p>Error: {error}</p>
{:else}
  {#each flights as flight (flight.id)}
    <div class="flight-card">
      <p><strong>{flight.airline}</strong> - {flight.departure} to {flight.arrival}</p>
      <p>${flight.price}</p>
      <button on:click={() => bookFlight(flight)}>Book</button>
    </div>
  {:else}
    <p>No flights found. Try adjusting your search.</p>
  {/each}
{/if}

The {:else} inside the each block renders when flights is empty (no results). The outer :else if error handles errors, and the {#if loading} shows a spinner. This gives users clear feedback at every stage.

Without an empty state message, users see a blank screen and assume the page broke. With {:else}, they know the search completed but found nothing.

Common mistakes

Using array index as a key: We've emphasized this, but it deserves its own line. Never do {#each items as item, i (i)}. If your items don't have a natural ID, generate one (UUID, database ID, etc.).

Using a property that changes: If you use (item.name) as a key and the name is editable, you'll break the binding. Use an ID that never mutates.

Forgetting keys when you have animations: Your transition:fade or animate:flip will behave strangely if there's no key. The two go hand-in-hand.

Not handling empty state: Always add a {:else} clause. It's a small QoL improvement that prevents confusion.

Complex object destructuring without keys: If you destructure in the loop—{#each items as { id, name, price } (id)}—the key must still be specified and must be available after destructuring. This works, but keep it readable.

Key takeaways

  • Always use a stable identifier as the key in each blocks, typically (item.id). Never use the array index.
  • Keys maintain component identity across renders, preserving form state, animations, and focus.
  • Animations require keys to work correctly. Without them, transitions and reordering animations attach to the wrong elements.
  • Use {:else} inside each to show a message when a list is empty, improving UX.
  • Test your lists by reordering, adding, and removing items in development. If checkboxes drift or state gets scrambled, you're missing or using the wrong key.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles