Identity Crisis

When items in a list change — reordered, added, removed — Svelte needs to know which DOM elements correspond to which data items. Without proper identification, updates can behave unexpectedly. Keys solve this.


What You’ll Learn

  • Why unkeyed lists can cause problems
  • How to add keys to each blocks
  • When keys matter most

The Problem Without Keys

Consider a list where items can be removed:

<script>
  let services = $state([
    { id: '1', name: 'Lawn Mowing' },
    { id: '2', name: 'Hedge Trimming' },
    { id: '3', name: 'Tree Pruning' }
  ]);
  
  function removeFirst() {
    services = services.slice(1);
  }
</script>

{#each services as service}
  <div class="service">
    <input value={service.name} />
    <span>{service.name}</span>
  </div>
{/each}

<button onclick={removeFirst}>Remove First</button>

Try this: type something in the first input, then click “Remove First.” What happens?

Without keys, Svelte matches by position. When you remove the first item:

  • The first DOM element stays, but now shows data from what was the second item
  • The typed input value stays in the first element
  • The result: wrong data in the wrong place

Adding Keys

Keys tell Svelte how to identify each item uniquely:

{#each services as service (service.id)}
  <div class="service">
    <input value={service.name} />
    <span>{service.name}</span>
  </div>
{/each}

The (service.id) at the end is the key expression. Now when you remove the first item:

  • Svelte knows which DOM element belongs to which service
  • The correct element is removed
  • Input values stay with their proper items

Key Requirements

Keys must be:

Unique — Each item in the list needs a different key value:

<!-- ✅ Good: IDs are unique -->
{#each services as service (service.id)}

<!-- ❌ Bad: Multiple items could have same price -->
{#each services as service (service.price)}

Stable — The same item should always have the same key:

<!-- ❌ Bad: Index changes when items are reordered -->
{#each services as service, index (index)}

<!-- ✅ Good: ID stays with the item -->
{#each services as service (service.id)}

Primitive — Keys should be strings or numbers:

<!-- ✅ Good: string key -->
{#each services as service (service.id)}

<!-- ✅ Good: slug as key -->
{#each services as service (service.slug)}

<!-- ❌ Avoid: object as key -->
{#each services as service (service)}

When Keys Matter Most

Keys are essential when:

Items can be reordered:

<script>
  let services = $state([...]);
  
  function sortByPrice() {
    services = [...services].sort((a, b) => a.price - b.price);
  }
</script>

{#each services as service (service.id)}
  <!-- ... -->
{/each}

Items can be removed:

<script>
  function removeService(id) {
    services = services.filter(s => s.id !== id);
  }
</script>

{#each services as service (service.id)}
  <div>
    {service.name}
    <button onclick={() => removeService(service.id)}>Remove</button>
  </div>
{/each}

Items contain form inputs:

{#each services as service (service.id)}
  <input bind:value={service.name} />
{/each}

Items have animations:

{#each services as service (service.id)}
  <div transition:slide>
    {service.name}
  </div>
{/each}

BookIt Services with Keys

Update the services page to use keys:

<!-- filename: src/routes/services/+page.svelte -->
<script>
  const services = [
    { id: '1', slug: 'lawn-mowing', name: 'Lawn Mowing', price: 50 },
    { id: '2', slug: 'hedge-trimming', name: 'Hedge Trimming', price: 75 },
    { id: '3', slug: 'garden-consultation', name: 'Garden Consultation', price: 100 }
  ];
</script>

<h1>Our Services</h1>

{#each services as service (service.id)}
  <article class="service-card">
    <h2>{service.name}</h2>
    <p>${service.price}</p>
    <a href="/services/{service.slug}">View Details</a>
  </article>
{/each}

For static lists that never change, keys are technically optional. But adding them is good practice — you never know when a list might become dynamic.


Index as Key: When It’s OK

Using the index as a key is acceptable when:

  • The list is static (never changes)
  • Items are never reordered
  • Items are only added to the end
<!-- OK for a static, append-only list -->
{#each logMessages as message, index (index)}
  <p>{message}</p>
{/each}

But prefer proper IDs when available. They’re more robust.


Generating IDs

If your data doesn’t have IDs, you can generate them:

<script>
  let nextId = 1;
  
  let items = $state([]);
  
  function addItem(name) {
    items = [...items, { id: nextId++, name }];
  }
</script>

Or use a library like uuid for globally unique IDs:

import { v4 as uuidv4 } from 'uuid';

const newItem = { id: uuidv4(), name: 'New Service' };

Common Mistakes

Using Non-Unique Keys

<!-- ❌ Category isn't unique — multiple items share it -->
{#each services as service (service.category)}

Svelte will warn about duplicate keys. Each item must have a distinct key.

Using Index for Dynamic Lists

<!-- ❌ Index changes when items are removed -->
{#each services as service, i (i)}
  <button onclick={() => remove(i)}>Remove</button>
{/each}

After removal, all indices shift, causing mismatched updates.

Forgetting Keys Entirely

For simple demos, forgetting keys might not cause visible issues. But when you add interactivity, animations, or form inputs, problems emerge. Add keys from the start.


Summary

Keys help Svelte track which DOM elements belong to which data items. Use unique, stable identifiers — typically an id field. This ensures correct behavior when lists are filtered, sorted, or modified.

Key takeaways:

  • Add keys with {#each items as item (item.id)}
  • Keys must be unique and stable
  • Always use keys for dynamic, interactive lists

Next Steps

You can render lists and show/hide content. Now let’s combine them. Continue with Filter Services by Category to build interactive filtered displays.