A visible lifetime for local state
I want to dive into why the recent Svelte update (declaration tags) is significant. If there’s anything to take away from this, it’s that these declarations give local state a visible lifetime. And if declaration tags is not very clear - think template-scoped state.
When faced with the situation of needing state for each item in an iterable, there tend to be two common options.
One is to keep state for each item in some sort of reactive lookup table.
<script>
import { SvelteMap } from 'svelte/reactivity';
let revealed = new SvelteMap();
</script>
{#each questions as question (question.id)}
<p>{question.prompt}</p>
{#if revealed.get(question.id)}
<p>{question.answer}</p>
{:else}
<button onclick={() => revealed.set(question.id, true)}>
Reveal answer
</button>
{/if}
{/each} The other is to extract the item into its own component.
<!-- Questions.svelte -->
{#each questions as question (question.id)}
<Question {question} />
{/each} <!-- Question.svelte -->
<script>
let { question } = $props();
let revealed = $state(false);
</script>
<p>{question.prompt}</p>
{#if revealed}
<p>{question.answer}</p>
{:else}
<button onclick={() => revealed = true}>Reveal answer</button>
{/if} Extracting a component is often the idiomatic choice, and not a bad solution at all. Once similar items need to encapsulate their own state, the itch to create a component appears naturally.
However, being forced to create an abstraction is not the same as the abstraction being warranted. In this example, the component exists primarily to give one boolean a lifetime.
Now consider the same example with declaration tags.
{#each questions as question (question.id)}
{let revealed = $state(false)}
<p>{question.prompt}</p>
{#if revealed}
<p>{question.answer}</p>
{:else}
<button onclick={() => revealed = true}>Reveal answer</button>
{/if}
{/each} The distinction is that revealed is named and usable only within this render block. It survives updates and reordering while the keyed question remains rendered. When that question leaves the block, its state leaves with it.
The component version gives us the same lifetime, but also requires a name and an interface. Declaration tags let us choose that lifetime without creating an abstraction solely to contain it.
The same idea applies to other kinds of transient UI state. Consider a menu where each meal is available in different sizes. Each meal owns its selected size, while a shared function adds that selection to the order.
<script>
function addToOrder(meal, size) {
// Add the selected meal and size
}
</script>
{#each meals as meal (meal.id)}
{let size = $state('regular')}
<h2>{meal.name}</h2>
<button onclick={() => size = 'regular'}>Regular</button>
<button onclick={() => size = 'large'}>Large</button>
<button onclick={() => addToOrder(meal, size)}>
Add {size}
</button>
{/each} The point is not to avoid components, but to let abstractions emerge from meaningful patterns instead of creating them merely to give local state somewhere to live.