# Svelte from Zero - The Framework That Compiles Away

> How Svelte actually works - a compiler that turns your components into surgical DOM updates at build time - and how runes, templates, and components build on that one idea.


---

# Svelte from Zero - The Framework That Compiles Away

React ships a runtime that diffs descriptions of your UI. Vue ships a runtime that tracks who reads
what. Svelte's bet is stranger and simpler: **do the framework's thinking at build time.** Your
component is compiled - analyzed like source code, because it is source code - into small,
direct instructions: "when `count` changes, update this one text node." At runtime there's no
diffing and no dependency graph to consult, because the compiler already worked out exactly what
depends on what.

Full disclosure of bias: the site you're reading is built with Svelte. We picked it for the same
reasons this guide will show you - and we'll flag its trade-offs with the same candor as everyone
else's.

## How to read this

- **In a panic right now?** Jump to [Phase 7: When Svelte Breaks](07-when-it-breaks.md) and use the
  cheat-card.
- **Want it to finally make sense?** Read in order - phase 2 (runes) is the foundation the rest
  stands on.

## The phases

1. **[What Svelte Actually Is](01-what-svelte-actually-is.md)** - the compiler idea, and the
   anatomy of a `.svelte` file.
2. **[Runes: State That Compiles](02-runes-state-that-compiles.md)** - `$state`, `$derived`, and
   how reactivity works when it's a language feature.
3. **[Template Logic](03-template-logic.md)** - `{#if}`, `{#each}` with keys, `{#await}`, and
   `bind:`.
4. **[Components: Props, Callbacks, and Snippets](04-components-props-snippets.md)** - talking
   down, up, and passing markup.
5. **[Sharing State](05-sharing-state.md)** - lifting, context, and shared state in `.svelte.js`
   modules.
6. **[Effects, Lifecycle, and Fetching](06-effects-lifecycle-fetching.md)** - `$effect` used
   sparingly, `onMount`, and data loading.
7. **[When Svelte Breaks](07-when-it-breaks.md)** - lost reactivity, effect loops, and the
   compiler's own error messages, decoded.
8. **[Where to Go Next](08-where-to-go-next.md)** - SvelteKit and the ecosystem, sized fairly.

> Deliberately deferred to follow-up guides: SvelteKit itself (routing, server loading, form
> actions), transitions and animation (a genuine Svelte strength), stores in depth, and testing.

> 📝 A dialect note before you start: this guide teaches **Svelte 5 with runes** (`$state`,
> `$derived`) - the current syntax. Code in the wild often uses the older dialect (`let` +
> `$:` labels, `export let`); phase 2 and 4 include translation notes so you can read both.


---

# What Svelte Actually Is

Every frontend framework answers the same question: *when data changes, how does the screen find
out?* React's answer is re-render and diff. Vue's is track reads and notify. Svelte's answer is the
one that sounds like cheating: **figure it out before the app ever runs.**

## A compiler, not a library

A `.svelte` file is not JavaScript that imports a framework - it's source code *for* a compiler.
At build time, Svelte reads your component, sees exactly which pieces of markup depend on which
pieces of state, and emits plain JavaScript that updates those pieces directly:

```html
<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>
```

What the compiler emits (conceptually - the real output is more careful):

```js
// "when count changes, set this text node" - decided at BUILD time
button.addEventListener('click', () => {
  count++;
  textNode.data = `Clicked ${count} times`;
});
```

*What just happened:* there is no framework in that output. No virtual DOM to build, no diff to
run, no dependency graph to consult at runtime - the compiler *already knew* that `count` affects
exactly one text node, and wrote the update by hand, so to speak. That's the whole trick, and it
has two visible consequences:

- **Less JavaScript shipped.** You ship your compiled components plus a small runtime, not a
  framework that must be able to handle any component generically.
- **No reconciliation work at runtime.** Updates go straight to the affected nodes.

```mermaid
flowchart LR
  SRC[.svelte source] -->|build time| C{Svelte compiler}
  C -->|"analyzes: what depends on what"| OUT[plain JS with direct DOM updates]
  OUT -->|runtime| B[browser: no diffing, no framework loop]
```

📝 **Terminology:** you'll hear "Svelte has no virtual DOM" as its tagline. Now you know what it
actually means: the *work* a virtual DOM does (figuring out what changed) is done once, at compile
time, instead of on every update at runtime. The trade-off is equally real: a compiler can only
analyze what it can see, which is why Svelte state needs to be *declared* as reactive (`$state` -
next phase) rather than being any old variable.

## The .svelte file

Like Vue, Svelte uses single-file components; the anatomy is nearly identical:

```html
<script>
  // logic - plain JavaScript (or TypeScript with lang="ts")
  let name = $state('world');
</script>

<!-- markup: HTML at the top level, no wrapper element required -->
<p class="greeting">Hello, {name}!</p>

<style>
  /* scoped BY DEFAULT - these rules affect only this component */
  .greeting { color: teal; }
</style>
```

Three differences from its neighbors worth noticing:

- **Markup is top-level.** No `<template>` wrapper, no single-root rule - the HTML just sits there.
- **`{expression}`** - single curly braces interpolate any JavaScript expression into text or
  attributes: `<img src={product.imageUrl} alt={product.name} />`. One syntax for both, no `:`
  prefix or mustache distinction to remember.
- **Styles are scoped by default.** No `scoped` attribute needed - leaking styles is the opt-in
  (`:global(...)`), not the accident.

## Booting a project

```console
$ npx sv create my-app
┌  Welcome to the Svelte CLI!
◇  Which template would you like?  SvelteKit minimal
◇  Add type checking with TypeScript?  Yes
└  Project created

$ cd my-app && npm install && npm run dev

  VITE ready in 621 ms
  ➜  Local:   http://localhost:5173/
```

*What just happened:* the official scaffold creates a **SvelteKit** project even for learning -
SvelteKit is to Svelte what Next is to React (routing, server rendering; phase 8), but you can
ignore all of it for now: your components live in `src/routes/+page.svelte` and
`src/lib/*.svelte`, and everything this guide teaches is pure Svelte that works the same anywhere.

For what it's worth: the site you're reading runs on exactly this stack, serving these guides with
the compiled-away approach this phase just described. When phase 8 weighs SvelteKit, it's a review
from a resident, not a tourist.

## Recap

1. Svelte is a compiler: dependency analysis happens at build time, and the output updates the DOM
   directly - no virtual DOM, no runtime diffing.
2. The trade: less shipped JS and no reconciliation cost, in exchange for reactivity being explicit
   (declared state) so the compiler can see it.
3. A `.svelte` file = `<script>` + top-level markup + auto-scoped `<style>`; `{expr}` interpolates
   everywhere.
4. The official scaffold gives you SvelteKit; plain Svelte knowledge transfers into it unchanged.

```quiz
[
  {
    "q": "Where does Svelte figure out which DOM nodes a piece of state affects?",
    "choices": [
      "At runtime, by diffing the new render against the previous one",
      "At runtime, by tracking which templates read which data",
      "At build time - the compiler analyzes the component and emits direct update code",
      "In the browser's devtools protocol"
    ],
    "answer": 2,
    "why": [
      "That's React's model - Svelte ships no diffing machinery at all.",
      "That's Vue's model - Svelte has no runtime tracking system to consult.",
      null,
      "Devtools observe your app; they don't participate in rendering."
    ],
    "explain": "Svelte is a compiler: the depends-on-what analysis happens once at build time, and the output is plain JavaScript that updates exactly the affected nodes."
  },
  {
    "q": "What does \"Svelte has no virtual DOM\" actually mean in practice?",
    "choices": [
      "Svelte can't update the DOM dynamically",
      "The change-detection work a virtual DOM does at runtime was done once by the compiler instead",
      "Svelte manipulates a hidden iframe instead of the real DOM",
      "Svelte apps must be fully static"
    ],
    "answer": 1,
    "why": [
      "Updates are fully dynamic - they're just precomputed rather than discovered by diffing.",
      null,
      "No iframes involved - the output touches the real DOM directly.",
      "Svelte apps are as interactive as any other framework's."
    ],
    "explain": "A virtual DOM exists to answer 'what changed?' at runtime. Svelte answers it at build time, so the runtime just executes the precomputed updates."
  }
]
```


---

# Runes: State That Compiles

Phase 1 said Svelte's trade-off out loud: for the compiler to precompute updates, it has to *see*
which variables are reactive. Runes are how you tell it. They look like function calls -
`$state()`, `$derived()` - but they're not functions you could import or reimplement: they're
**keywords for the compiler**, markers in the source that change what code gets generated.

## $state: a variable the compiler watches

```html
<script>
  let count = $state(0);

  function add() {
    count++;                    // plain mutation - the compiled code updates the DOM
  }
</script>

<button onclick={add}>Clicked {count} times</button>
```

*What just happened:* `$state(0)` declares `count` as reactive. From then on you use it like any
variable - read it bare, mutate it with `++` or `=`. The compiler saw the declaration, saw the
usage in the markup, and generated the update wiring. No setter function, no `.value`, no
container object in your way.

Objects and arrays go deeper:

```html
<script>
  let todos = $state([
    { text: 'Learn runes', done: true },
    { text: 'Ship something', done: false },
  ]);

  function addTodo(text) {
    todos.push({ text, done: false });      // push works. Mutation works.
  }
  function toggle(todo) {
    todo.done = !todo.done;                 // nested mutation works too
  }
</script>
```

📝 **Terminology:** `$state` on an object or array wraps it in a **deeply reactive proxy** - every
nested read and write is observed (the same proxy machinery Vue uses, if you've read that guide,
here in service of the compiler's wiring). That's why `push` and nested assignment update the
screen - no copy-and-replace choreography needed.

## $derived: values that follow

```html
<script>
  let todos = $state([...]);

  let remaining = $derived(todos.filter(t => !t.done).length);
  let allDone = $derived(remaining === 0);
</script>

<p>{remaining} left {allDone ? '- take a break!' : ''}</p>
```

*What just happened:* `$derived` declares a value computed *from* other reactive state. When
`todos` changes, `remaining` follows; when `remaining` changes, `allDone` follows - a chain the
compiler wires up. Derived values are read-only (assigning to one is a compile error), cached, and
recomputed only when a dependency actually changed.

For derivations too big for one expression, `$derived.by(() => { ... })` takes a function. The
discipline is the same one every framework preaches: **if a value can be computed from other
state, derive it - don't store a second copy and try to keep it in sync.**

💡 **Key point:** notice what you're *not* managing: no dependency arrays, no subscription calls,
no memoization decisions. You declare which variables are state and which are derivations; the
compiler derives the graph from your actual reads. The runtime cost of getting this wrong is
mostly replaced by compile errors - a fair summary of Svelte's whole personality.

## Why runes have rules

Because runes are compiler markers, they only exist where the Svelte compiler runs:

- **`.svelte` files** - components.
- **`.svelte.js` / `.svelte.ts` files** - plain modules *opted in* to compilation, the home of
  shared state (phase 5).

Write `$state` in a regular `.js` file and you get an error, not a quiet failure - the compiler
never ran there, so the rune is just an undefined identifier. Same reason you can't do
`const s = $state; s(0)` or pass runes around: they're syntax, not values.

⚠️ **Gotcha:** the classic reactivity leak in Svelte 5 is **destructuring state**:

```js
let user = $state({ name: 'Ada', plan: 'pro' });

let { name } = user;      // ✗ copies the current string OUT of the proxy - dead
name = 'Grace';           // updates nothing

user.name = 'Grace';      // ✓ mutate through the object
let name2 = $derived(user.name);  // ✓ or derive a live view
```

Destructuring copies the value at that instant; the copy has no connection to the proxy, and
nothing re-runs the destructuring later. Reads in markup (`{user.name}`) are safe - the compiled
code re-reads through the proxy. The rule: **access state through its object, or derive; never
park a snapshot in a plain variable and expect it to stay live.**

## Reading the old dialect

You'll meet pre-rune Svelte constantly (including, at the time of writing, parts of this site's
own codebase). The translation table:

| Legacy (Svelte 3/4) | Runes (Svelte 5) | Notes |
|---|---|---|
| `let count = 0;` | `let count = $state(0);` | top-level `let` was implicitly reactive |
| `$: doubled = count * 2;` | `let doubled = $derived(count * 2);` | `$:` labels were the derive/effect syntax |
| `$: console.log(count);` | `$effect(() => console.log(count));` | the side-effect use of `$:` (phase 6) |
| `export let name;` | `let { name } = $props();` | props (phase 4) |
| `on:click={fn}` | `onclick={fn}` | events became plain attributes |

The old dialect still compiles (Svelte 5 supports it per-component), so nothing you inherit is
broken - it's one more dialect to read, like Vue's Options API.

## Recap

1. `$state` declares reactive variables; use them like normal JavaScript - mutation included.
   Objects/arrays become deeply reactive proxies.
2. `$derived` declares computed values; chains resolve automatically; derive instead of storing
   copies.
3. Runes are compiler syntax: they work only in `.svelte` / `.svelte.js` files and can't be
   aliased or passed around.
4. Destructuring state parks a dead snapshot in a variable - go through the object, or `$derived`
   a live view.
5. Legacy dialect: `let` was state, `$:` was derived/effect, `export let` was props - read it
   fluently, write runes.

```quiz
[
  {
    "q": "Why does $state work in App.svelte and cart.svelte.js, but throw an error in utils.js?",
    "choices": [
      "utils.js is missing an import of $state from 'svelte'",
      "Runes are compiler syntax, and the compiler only processes .svelte and .svelte.js files",
      "State is only allowed inside components for architectural reasons",
      "The file needs to be renamed to utils.ts"
    ],
    "answer": 1,
    "why": [
      "There's nothing to import - runes aren't values; that's exactly what makes them file-bound.",
      null,
      ".svelte.js modules hold state outside components on purpose - the rule is about compilation, not architecture.",
      "TypeScript vs JavaScript is irrelevant - .svelte.ts works, .ts doesn't."
    ],
    "explain": "A rune is an instruction to the compiler, not a function. In files the compiler never touches, $state is just an undefined name - hence the loud error."
  },
  {
    "q": "let { name } = $state-proxied user, and later name = 'Grace' changes nothing on screen. What's the mechanism?",
    "choices": [
      "Strings are immutable in JavaScript, so the assignment fails",
      "Destructuring copied the value out of the proxy at that moment - the local variable has no connection to the tracked object",
      "The compiler only tracks variables declared with $state directly",
      "name collided with a reserved template variable"
    ],
    "answer": 1,
    "why": [
      "The assignment succeeds - to a local variable that nothing observes.",
      null,
      "Close, but the real distinction is the copy: reads through user.name stay live even in helper code the compiler processed.",
      "No such reserved names exist."
    ],
    "explain": "The proxy observes access through itself. A destructured primitive is a snapshot with no link back - mutate user.name, or declare a $derived for a live read."
  },
  {
    "q": "In legacy Svelte code you see $: total = price * qty. What is the runes translation?",
    "choices": [
      "let total = $state(price * qty)",
      "let total = $derived(price * qty)",
      "$effect(() => total = price * qty)",
      "let total = price * qty"
    ],
    "answer": 1,
    "why": [
      "$state would snapshot the product once - it wouldn't follow price or qty afterward.",
      null,
      "An effect assigning state recreates the derived behavior manually - and phase 6 explains why that pattern should be avoided (and Svelte will warn).",
      "A plain let computes once at init and never again."
    ],
    "explain": "$: with a pure computation was the old derived syntax. $derived is its direct successor: recomputed when dependencies change, cached, read-only."
  }
]
```


---

# Template Logic

Svelte markup is HTML plus a small set of block tags - `{#if}`, `{#each}`, `{#await}` - and the
`bind:` directive. Where React uses JavaScript expressions (`map`, ternaries) and Vue uses
attributes (`v-if`, `v-for`), Svelte gives control flow its own syntax, opened with `{#...}` and
closed with `{/...}`. Five minutes of syntax, then the gotchas that actually matter.

## Branching: {#if}

```html
<script>
  let cart = $state([]);
</script>

{#if cart.length === 0}
  <p>Your cart is empty.</p>
{:else if cart.length < 10}
  <p>{cart.length} items.</p>
{:else}
  <p>Bulk order!</p>
{/if}
```

A false branch **unmounts** its contents - DOM removed, component state inside destroyed, same
semantics as conditional rendering everywhere. There's no built-in "hide with CSS instead"
directive (Vue's `v-show`); when you need state to survive a frequent toggle, keep the element and
toggle a class: `<div class:hidden={!open}>` (that `class:` shorthand toggles a class by boolean -
you'll use it constantly).

## Repeating: {#each} and the key

```html
<script>
  let todos = $state([
    { id: 1, text: 'Learn each blocks' },
    { id: 2, text: 'Remember the key' },
  ]);
</script>

<ul>
  {#each todos as todo (todo.id)}
    <li>{todo.text}</li>
  {:else}
    <li>Nothing to do. Suspicious.</li>
  {/each}
</ul>
```

*What just happened:* one `<li>` per item, with two pieces of syntax worth naming. The
parenthesized `(todo.id)` is the **key expression** - the same identity contract as every
framework: it's how Svelte matches old items to new ones when the array changes, so a reorder
*moves* DOM instead of rewriting every row. And `{:else}` inside an each block renders when the
array is empty - the empty-state pattern built into the syntax.

⚠️ **Gotcha:** the key is *syntactically optional*, and that's a trap: without it, Svelte updates
each-block rows **by position**. Delete the first todo and every row's DOM shifts contents up one -
fine for pure text, state-corrupting the moment rows hold inputs, checkboxes, or component state
(the typed text stays in row one while the labels shift). If the list can ever reorder, insert, or
delete: key it with a stable id. `(index)` as the key is the same bug with extra steps.

## Promises in markup: {#await}

The block with no direct equivalent in React or Vue:

```html
<script>
  let productPromise = $state(fetchProduct(42));
</script>

{#await productPromise}
  <p>Loading…</p>
{:then product}
  <h2>{product.name}</h2>
{:catch error}
  <p>Couldn't load: {error.message}</p>
{/await}
```

*What just happened:* the three states of a promise - pending, resolved, rejected - each get a
branch, right in the markup. No loading flag, no error state variable, no effect: hand the block a
promise and it renders the appropriate branch as the promise settles. Refetching is reassigning:
`productPromise = fetchProduct(newId)` swaps in a new promise and the block starts over at
pending. (For fetch-on-navigation, SvelteKit's load functions - phase 8 - are the fuller answer;
`{#await}` covers the in-component cases.)

## Two-way forms: bind:

```html
<script>
  let email = $state('');
  let agreed = $state(false);
  let plan = $state('free');
</script>

<input type="email" bind:value={email} placeholder="you@example.com" />
<label><input type="checkbox" bind:checked={agreed} /> I agree</label>
<select bind:value={plan}>
  <option value="free">Free</option>
  <option value="pro">Pro</option>
</select>

<p>Signing up {email || '…'} for the {plan} plan.</p>
<button disabled={!agreed || !email.includes('@')}>Sign up</button>
```

*What just happened:* `bind:value` wires the input to the state variable in both directions -
value in, keystrokes back. Checkboxes bind `checked`, selects bind `value`, and
`<input type="number">` binds through `bind:value` with the string-to-number coercion handled.
Validation is then just expressions over state, as the disabled button shows.

`bind:` reaches beyond forms, worth knowing exists: `bind:this={el}` gives you the DOM element
itself (Svelte's ref mechanism, for focus management or third-party libraries), and read-only
bindings like `bind:clientWidth` observe layout without a ResizeObserver in sight.

Events, for completeness, are plain attributes in the runes dialect: `onclick={handler}`,
`onsubmit={save}`. One habit transfers from every framework: pass the function. To preventDefault,
wrap it - `onsubmit={e => { e.preventDefault(); save(); }}` (the old dialect's `|preventDefault`
modifier is gone).

## Recap

1. `{#if}` unmounts on false; for state-preserving toggles use `class:` and CSS.
2. `{#each list as item (item.id)}` - the key is optional syntax but mandatory practice for any
   list that changes shape; `{:else}` handles empty.
3. `{#await}` renders a promise's three states declaratively; reassign the promise to restart.
4. `bind:value` / `bind:checked` are two-way form wiring; `bind:this` is the element ref.
5. Events are attributes (`onclick`); no modifiers - call `preventDefault` yourself.

```quiz
[
  {
    "q": "A todo list's rows contain checkboxes. Deleting the first todo makes the wrong rows show as checked. The each block has no key expression. What happened?",
    "choices": [
      "The delete handler mutated the array, which Svelte doesn't track",
      "Without a key, rows update by position - the DOM (and its checkbox state) stayed put while item contents shifted up",
      "Checkbox state needs bind:checked to survive deletions",
      "The {:else} branch interfered with row matching"
    ],
    "answer": 1,
    "why": [
      "Mutation is fine in Svelte - $state proxies track push/splice; the update happened, just positionally.",
      null,
      "bind:checked wires state to a row - but without keys, the row itself is reassigned to a different item.",
      "{:else} only renders when the list is empty; it plays no role in matching."
    ],
    "explain": "Unkeyed each blocks match old and new items by index. Row DOM gets reused for whatever item now sits at that index - add (todo.id) so identity follows the item."
  },
  {
    "q": "What does the {#await} block replace from the classic client-side fetch pattern?",
    "choices": [
      "The fetch call itself",
      "The loading flag, error state, and the conditional rendering between them",
      "The need for async functions",
      "Request cancellation"
    ],
    "answer": 1,
    "why": [
      "You still create the promise - the block consumes it.",
      null,
      "Async code is still async - the block just renders its states.",
      "Cancellation still needs your own handling (or a data layer) - the block renders whatever promise it's given."
    ],
    "explain": "Pending, resolved, and rejected each get a markup branch, so the three state variables and their if/else chain disappear. The promise is the state."
  }
]
```


---

# Components: Props, Callbacks, and Snippets

A `.svelte` file is already a component; using one is an import and a tag. This phase is the
interface layer: data in (`$props`), events out (callback props), two-way when explicitly agreed
(`$bindable`), and markup in (snippets). If you've read the React or Vue guides, the shape is
familiar - Svelte's version is notable mostly for how little of it there is.

## $props: declared inputs

```html
<!-- ProductCard.svelte -->
<script>
  let { name, price, inStock = true } = $props();
</script>

<article class:dimmed={!inStock}>
  <h3>{name}</h3>
  <p>{(price / 100).toFixed(2)} €</p>
</article>
```

```html
<!-- parent -->
<script>
  import ProductCard from '$lib/ProductCard.svelte';
</script>

<ProductCard name="Kettle" price={4900} inStock={false} />
```

*What just happened:* `$props()` returns the props object, destructured with defaults in the
declaration - one line documents the component's inputs. Attributes pass strings; `{expressions}`
pass everything else - the same string-vs-expression rule as every framework, same
`price="4900"`-is-a-string trap included.

Wait - phase 2 said destructuring kills reactivity. `$props()` is the sanctioned exception: the
compiler treats these destructured names specially, keeping them live as the parent re-renders.
Rune magic, but *declared* magic, in the one place it's guaranteed.

Props are the parent's data on loan: assigning to a prop from inside the child triggers a runtime
warning (`ownership_invalid_mutation`) rather than silently diverging. The child's channel for
change requests is the next section. With TypeScript, type the destructure and the contract is
checked at build time:

```ts
let { name, price, inStock = true }: { name: string; price: number; inStock?: boolean } = $props();
```

## Events up: callback props

Svelte 5 dropped its old event-dispatch system for something with zero new concepts: **a callback
is just a prop that happens to be a function.**

```html
<!-- ProductCard.svelte -->
<script>
  let { name, price, onAddToCart } = $props();
</script>

<article>
  <h3>{name}</h3>
  <button onclick={() => onAddToCart(1)}>Add</button>
</article>
```

```html
<!-- parent -->
<ProductCard name="Kettle" price={4900} onAddToCart={qty => cart.add('kettle', qty)} />
```

*What just happened:* data down, function down, call up - the React pattern, name and all. The
child announces intent by calling; the parent decides what it means. Legacy note: older code does
this with `createEventDispatcher()` and `on:addToCart` listeners - deprecated but everywhere in
the wild; mentally translate `dispatch('x', detail)` to `onX(detail)`.

## $bindable: two-way, by consent

Sometimes parent and child genuinely co-own a value - a search input component, a rating widget.
Svelte allows `bind:` on component props, but only if the child *opts in*:

```html
<!-- StarRating.svelte -->
<script>
  let { value = $bindable(0) } = $props();
</script>

{#each [1, 2, 3, 4, 5] as n}
  <button onclick={() => value = n}>{n <= value ? '★' : '☆'}</button>
{/each}
```

```html
<!-- parent -->
<StarRating bind:value={rating} />
```

*What just happened:* `$bindable` marks the prop as writable-from-within; the parent's `bind:`
links it to their own state. Without the rune, `bind:` on that prop is a compile error - two-way
flow exists, but it's a declared contract, never an ambush. Use it for genuine form-like
components; everywhere else, callbacks keep the data flow one-way and traceable.

## Snippets: markup as a prop

The composition mechanism - a Card owning the frame while callers own the contents:

```html
<!-- Card.svelte -->
<script>
  let { title, children } = $props();
</script>

<section class="card">
  <h2>{title}</h2>
  {@render children()}
</section>
```

```html
<!-- parent -->
<Card title="Danger zone">
  <p>Deleting your account is permanent.</p>
  <button onclick={confirmDelete}>Delete</button>
</Card>
```

*What just happened:* content nested inside `<Card>` arrives as `children` - a **snippet**, a
chunk of renderable markup passed as a prop - and `{@render children()}` places it. For multiple
outlets, declare named snippets explicitly:

```html
<!-- parent -->
<PageLayout>
  {#snippet header()}<h1>Orders</h1>{/snippet}
  {#snippet footer()}<small>Updated hourly.</small>{/snippet}
  Order list goes here.
</PageLayout>

<!-- PageLayout.svelte -->
<script>
  let { header, footer, children } = $props();
</script>
<header>{@render header?.()}</header>
<main>{@render children()}</main>
<footer>{@render footer?.()}</footer>
```

The `?.()` renders the snippet only if the caller provided it - optional outlets in one character.
And because snippets are functions, they take parameters: a list component can hand each item back
to caller-supplied markup - `{#snippet row(item)}` in the parent, `{@render row(item)}` in the
child - which is the scoped-slot pattern with plain function semantics instead of new syntax.

📝 **Terminology:** legacy Svelte does all this with `<slot>` / `<slot name="x">` elements, like
Vue. Snippets replaced slots in the runes dialect; both render fine today, and the translation is
mechanical: default slot ↔ `children`, named slot ↔ named snippet, slot props ↔ snippet
parameters.

## Recap

1. `let { x, y = default } = $props()` - the one sanctioned reactive destructure; type it with TS
   for a checked contract.
2. Events are callback props: `onAddToCart={fn}` down, `onAddToCart(payload)` up. Dispatcher code
   is legacy.
3. `bind:` on a component prop requires the child's `$bindable` - two-way by explicit consent.
4. Snippets pass markup: `children` implicitly, `{#snippet name()}` for multiple outlets,
   parameters for the scoped case, `{@render x?.()}` for optional ones.
5. Props are on loan - mutating them warns; call the callback instead.

```quiz
[
  {
    "q": "In Svelte 5, how does a child tell its parent the user clicked Save?",
    "choices": [
      "createEventDispatcher and dispatch('save')",
      "Call the onSave function the parent passed as a prop",
      "Mutate a $bindable flag the parent watches",
      "Emit through a shared store"
    ],
    "answer": 1,
    "why": [
      "That's the deprecated legacy system - still read it in old code, don't write it.",
      null,
      "Bindable exists for co-owned values, not event signaling - a save click is intent, not shared state.",
      "A store for a parent-child signal is global machinery for a local conversation."
    ],
    "explain": "Svelte 5 events-up are just function props: the parent hands down onSave, the child calls it with any payload. No dispatcher concept needed."
  },
  {
    "q": "bind:query={search} on your SearchBox component fails to compile. What's missing?",
    "choices": [
      "The parent must also declare $bindable",
      "The child must declare the prop as query = $bindable() - two-way binding requires the child's opt-in",
      "bind: only works on DOM elements, never components",
      "The prop must be named value for binding to work"
    ],
    "answer": 1,
    "why": [
      "The parent's side is just bind: - the consent lives in the child's declaration.",
      null,
      "Component binding is fully supported - gated behind $bindable.",
      "Any prop name binds, once bindable."
    ],
    "explain": "Two-way flow is a contract both sides sign: the child marks the prop $bindable, then the parent may bind:. Without the rune, the compiler refuses - no accidental two-way."
  },
  {
    "q": "A Table component should let callers control each row's markup while it handles sorting and pagination. Which mechanism fits?",
    "choices": [
      "A rowHtml string prop the caller formats",
      "A snippet parameter: the caller passes {#snippet row(item)}, the Table does {@render row(item)} per item",
      "A $bindable rows prop",
      "The caller wraps the Table and renders rows above it"
    ],
    "answer": 1,
    "why": [
      "HTML strings mean no reactivity, no components inside rows, and an injection footgun.",
      null,
      "Bindable shares a value both ways - it can't carry markup.",
      "Then the Table isn't rendering the rows at all, so its sorting and pagination decorate nothing."
    ],
    "explain": "Snippets with parameters are the scoped-slot pattern: the child owns iteration and logic, hands each item back to caller-supplied markup. Composition without new syntax - snippets are functions."
  }
]
```


---

# Sharing State

Two components need the same data: the search box and the results list, the cart icon and the cart
page. Svelte's answers are ranked like every framework's - keep it local, lift it, then reach for
the shared mechanisms - but the shared tier has a distinctly Svelte flavor: **state lives in
compiled modules**, not in a bolted-on store library. That's a genuine convenience with one sharp
edge, and this phase covers both.

## First resort: lift it

```html
<!-- SearchPage.svelte -->
<script>
  import SearchBox from '$lib/SearchBox.svelte';
  import ResultsList from '$lib/ResultsList.svelte';

  let query = $state('');
</script>

<SearchBox bind:value={query} />
<ResultsList {query} />
```

*What just happened:* the state lives in the closest common parent; one child binds it (a
`$bindable` input component, phase 4), the other reads it as a prop. One source of truth, and the
two can never disagree. (`{query}` is shorthand for `query={query}` - you'll see it everywhere.)
When two components with *separate copies* of "the same" state drift apart, this is the refactor:
move it up, pass it down, delete the copies.

## The Svelte move: state in a .svelte.js module

For state whose readers are scattered across the app - the cart, the session, preferences -
phase 2's compiled-module trick becomes the architecture:

```js
// src/lib/cart.svelte.js
export const cart = $state({ items: [] });

export function addItem(product, qty = 1) {
  const line = cart.items.find(i => i.product.id === product.id);
  if (line) line.qty += qty;
  else cart.items.push({ product, qty });
}

export function itemCount() {
  return cart.items.reduce((n, i) => n + i.qty, 0);
}
```

```html
<!-- any component, anywhere -->
<script>
  import { cart, addItem, itemCount } from '$lib/cart.svelte.js';
</script>

<span>Cart ({itemCount()})</span>
```

*What just happened:* a plain module exports a `$state` object and the functions that mutate it.
Every importer sees the same object; a mutation from the product page updates the header's badge,
because the badge's markup reads through the same proxy. State plus its update logic in one
importable file - most of what a store library exists for, in vanilla Svelte. (If you've read the
Vue guide: this is a Pinia store's shape, without Pinia.)

⚠️ **Gotcha - the export rule.** Notice the module exports an *object* and mutates its
properties. Export a reassignable primitive instead and you hit a wall:

```js
// counter.svelte.js
export let count = $state(0);        // compile error: cannot export reassigned state
export function increment() { count++; }
```

Why the compiler refuses: importers of a rebound `let` would capture a stale binding - the exact
dead-snapshot problem from phase 2, at module scale - so Svelte makes it a build error instead of
a silent bug. Two clean shapes exist: **wrap in an object and mutate properties**
(`export const counter = $state({ value: 0 })`), or **keep state private and export accessor
functions**. Either way, reads go through something the proxy can intercept.

## Context: per-tree state without prop threading

Module state is app-global - one cart for everyone. Sometimes you want one instance *per subtree*:
this form's state shared by its field components, this accordion group's open-item tracker. That's
the context API:

```html
<!-- Accordion.svelte -->
<script>
  import { setContext } from 'svelte';

  const state = $state({ openId: null });
  setContext('accordion', state);
</script>

{@render children()}
```

```html
<!-- AccordionItem.svelte, any depth below -->
<script>
  import { getContext } from 'svelte';

  let { id, children } = $props();
  const accordion = getContext('accordion');
</script>

<button onclick={() => accordion.openId = accordion.openId === id ? null : id}>
  {@render children()}
</button>
```

*What just happened:* the parent placed a reactive object into context; descendants at any depth
retrieved *the same object* - no prop threading through layers that don't care. Two accordions on
one page each provide their own context, so their states don't collide - the thing module state
can't do. Rules worth knowing: `setContext`/`getContext` run during component setup only (not in
handlers or effects), and lookups walk *up* the tree - siblings can't see each other's context.

## What about stores?

Pre-runes Svelte shared state through **stores** - `writable(0)`, subscribed with a `$store`
prefix in markup. They still work, they're all over existing codebases (including this site's),
and a few niches still want them (interop with libraries built on the store contract). But for new
code, module `$state` plus context covers the territory with fewer concepts. Translation for
reading legacy: `writable(x)` ≈ module `$state`, `$storeName` in markup ≈ reading the state
object, `derived(...)` ≈ `$derived`.

## Choosing, in one table

| Situation | Reach for |
|---|---|
| One component cares | `$state` right there |
| Siblings need it | Lift to the common parent |
| A widget tree needs its own instance | Context |
| The whole app shares one instance | `.svelte.js` module state |
| Legacy code / store-based libraries | Stores - read fluently, write runes |

## Recap

1. Lift first: closest common parent, props/bindings down. Two copies of one truth always drift.
2. `.svelte.js` modules hold app-wide state as `$state` + mutation functions - store-library
   ergonomics, zero dependencies.
3. Never export reassignable state - export an object you mutate, or accessor functions; the
   compiler enforces it.
4. Context = per-subtree instances, set during component setup, visible only downward.
5. Stores are the legacy tier: recognize `writable`/`$store` on sight, prefer runes for new code.

```quiz
[
  {
    "q": "export let count = $state(0) in a .svelte.js module fails to compile. What's the compiler protecting you from?",
    "choices": [
      "Primitives can't be reactive in modules - only objects can",
      "Importers would capture a stale binding when count is reassigned - the dead-snapshot bug, promoted to module scale",
      "Module state must be read-only by design",
      "$state is not allowed in .svelte.js files"
    ],
    "answer": 1,
    "why": [
      "A primitive is fine as long as nothing reassigns the exported binding - the object wrapper exists to give mutations an interceptable home.",
      null,
      "Module state is meant to be mutated - through object properties or exported functions.",
      ".svelte.js files exist precisely to host runes."
    ],
    "explain": "Reassigning an exported let would leave importers holding yesterday's value with no proxy in the path. The build error replaces what would otherwise be phase 2's silent disconnection."
  },
  {
    "q": "Two independent Wizard components on one page each need their own shared step-state for their child panels. Module state or context?",
    "choices": [
      "Module state - it's the modern Svelte way to share",
      "Context - each Wizard provides its own instance to its own subtree",
      "Either works identically",
      "Neither - the panels should use $bindable props"
    ],
    "answer": 1,
    "why": [
      "One module = one instance app-wide: both wizards would fight over the same step counter.",
      null,
      "They differ exactly here - global singleton versus per-tree instance.",
      "Bindable props work for direct parent-child pairs, but threading through every panel layer is the problem context removes."
    ],
    "explain": "Module state is a singleton; context is scoped to the providing component's subtree. Multiple instances of a widget each needing private shared state is the context case."
  }
]
```


---

# Effects, Lifecycle, and Fetching

Everything so far - state, derivations, templates - lives inside Svelte's world, where the
compiler wires changes to updates. But apps have to talk to the world outside: timers, network,
`localStorage`, chart libraries, the document title. `$effect` is that bridge, and it comes with
the same warning label as its cousins in React and Vue: **the moment you use an effect to manage
your own state instead of the outside world, you've built a bug.** This phase draws the line
precisely.

## $effect: the shape

```html
<script>
  let query = $state('');

  $effect(() => {
    document.title = query ? `Results for "${query}"` : 'Search';
  });
</script>
```

*What just happened:* the effect ran after the component mounted, and re-runs whenever `query`
changes. Which state it depends on is discovered the Svelte way - **by what the function actually
reads** - no dependency array to maintain, same auto-tracking as `$derived`, applied to actions
instead of values.

Cleanup is the return value:

```html
<script>
  let seconds = $state(0);

  $effect(() => {
    const id = setInterval(() => seconds++, 1000);
    return () => clearInterval(id);      // runs before re-run, and at unmount
  });
</script>
```

The returned function runs before the effect re-runs, and when the component unmounts - one
mechanism, both moments. Whatever an effect starts, its cleanup stops: intervals, listeners,
subscriptions, observers. An effect with a start and no stop is a leak with a delay on it.

## The rule: effects face outward

The most common misuse, in every framework, is the same:

```html
<script>
  let items = $state([]);

  // ✗ deriving state with an effect - and Svelte will throw at you
  let total = $state(0);
  $effect(() => {
    total = items.reduce((s, i) => s + i.price, 0);
  });

  // ✓ deriving state with a derivation
  let total = $derived(items.reduce((s, i) => s + i.price, 0));
</script>
```

Svelte is unusually opinionated here: writing to state that the same effect also (transitively)
depends on raises `state_unsafe_mutation` errors or effect-loop warnings, and even when it runs,
you've created a second source of truth that updates a beat late. The line to internalize:

💡 **Key point:** `$derived` answers "what *is* this value?" - `$effect` answers "what should
*happen* when this changes?" If the sentence ends in a value, derive. If it ends in an action on
the outside world (the DOM directly, a timer, the network, storage, a library), effect. Svelte's
docs say it plainly: if you're synchronizing state inside an effect, you almost always want
`$derived` instead.

## onMount and DOM-dependent setup

`$effect` doesn't run during server-side rendering (there's no browser to affect), and it first
runs after the component is in the DOM - so for most "when the component appears" work, `$effect`
*is* the mount hook. The older `onMount` import still exists and still matters in one common case:
it can be `async` (an effect's function can't be, since its return value must be the cleanup):

```html
<script>
  import { onMount } from 'svelte';

  let orders = $state(null);
  let error = $state(null);

  onMount(async () => {
    try {
      const res = await fetch('/api/orders');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      orders = await res.json();
    } catch (e) {
      error = e;
    }
  });
</script>

{#if error}<p>Couldn't load orders.</p>
{:else if !orders}<p>Loading…</p>
{:else}<ul>{#each orders as o (o.id)}<li>{o.ref}</li>{/each}</ul>{/if}
```

(Or skip the flags entirely: assign the promise to state and let phase 3's `{#await}` render the
three states. Both are idiomatic; `{#await}` is less code, explicit flags give you more control
over layout.)

## Refetch-on-change, with the race handled

Fetching *again* when a value changes is effect territory - an outside-world action triggered by
state. The out-of-order response race comes along, and cleanup is the fix:

```html
<script>
  let selectedId = $state(1);
  let product = $state(null);

  $effect(() => {
    const id = selectedId;               // read it: this effect now tracks selectedId
    let cancelled = false;

    fetch(`/api/products/${id}`)
      .then(r => r.json())
      .then(data => { if (!cancelled) product = data; });

    return () => { cancelled = true; };  // stale responses get ignored
  });
</script>
```

*What just happened:* switching `selectedId` from 1 to 2 runs the cleanup (cancelling 1's
in-flight interest) before re-running the effect for 2 - so if 1's response arrives late, it finds
`cancelled` true and touches nothing. Same pattern as React's effect flag and Vue's `onCleanup`,
wearing Svelte's return-function syntax.

⚠️ **Gotcha:** auto-tracking only registers what the effect reads **synchronously**. Reads inside
`.then` callbacks, `await` continuations, or `setTimeout` happen after tracking closed - they
don't subscribe the effect to anything. The `const id = selectedId` line isn't decoration: it's
the synchronous read that puts `selectedId` on the effect's dependency list. Read your triggers at
the top, then go async.

## Recap

1. `$effect` = auto-tracked side effects; dependencies are what it reads synchronously; the
   returned function is cleanup (pre-re-run and unmount).
2. Effects face outward - deriving state in an effect earns you `state_unsafe_mutation` and a
   stale copy. Values are `$derived`'s job.
3. `$effect` already covers "on mount" for browser work; `onMount` remains the async-friendly
   spelling for load-once fetches ({#await} being the low-ceremony alternative).
4. Refetch effects: read triggers at the top, cancel via cleanup, ignore stale responses.
5. Every start needs a stop - intervals and listeners without cleanup outlive their component.

```quiz
[
  {
    "q": "An effect fetches a product and reads selectedId only inside the .then callback. Changing selectedId doesn't refetch. Why?",
    "choices": [
      "Effects only run once unless given a dependency array",
      "Dependencies are tracked from synchronous reads - a read inside .then happens after tracking has closed, so the effect never subscribed to selectedId",
      "fetch calls can't be tracked by the compiler",
      "The effect needs to be marked async"
    ],
    "answer": 1,
    "why": [
      "Svelte effects have no dependency arrays - tracking is automatic, but only over synchronous reads.",
      null,
      "The fetch itself is irrelevant to tracking - what matters is when state is read.",
      "Effect functions can't be async at all (the return value must be the cleanup) - and that wouldn't fix the tracking window."
    ],
    "explain": "Auto-tracking records reads made while the effect body runs synchronously. Read your trigger at the top (const id = selectedId), then use it in async code freely."
  },
  {
    "q": "let total = $state(0) kept in sync by an $effect that reads items and writes total. Svelte complains, and the docs agree. What's the right shape?",
    "choices": [
      "Move the write into a setTimeout so it runs outside tracking",
      "let total = $derived(items.reduce(...)) - it's a value, not an action",
      "Split it into two effects, one reading and one writing",
      "Mark total as $bindable"
    ],
    "answer": 1,
    "why": [
      "Deferring the write dodges the error and keeps the stale-copy design - worse, not better.",
      null,
      "However you slice the effects, state-syncing-state remains a second source of truth.",
      "$bindable is a component-prop contract - unrelated to derivation."
    ],
    "explain": "The question 'what is total?' has an answer expressible from existing state - that's a derivation. Effects are for actions on the world outside Svelte's state graph."
  },
  {
    "q": "A component's effect starts a WebSocket connection. What must the effect also do?",
    "choices": [
      "Nothing - Svelte closes connections when components unmount",
      "Return a cleanup function that closes the socket",
      "Wrap the connection in onMount instead",
      "Store the socket in $state so it's tracked"
    ],
    "answer": 1,
    "why": [
      "Svelte tears down its own wiring - browser resources you open are yours to close.",
      null,
      "onMount changes when it starts, not who stops it - a cleanup is needed there too.",
      "Tracking a socket object does nothing; sockets aren't state, they're resources."
    ],
    "explain": "Effects that acquire resources return the release: the cleanup runs on re-run and unmount, so the socket's lifetime matches the component's."
  }
]
```


---

# When Svelte Breaks

Svelte's compiler catches at build time a lot of what other frameworks let you discover in
production - that's real. What's left over splits into two families: **silent staleness** (a
snapshot escaped the reactivity system - the same disease as everywhere, with Svelte-specific
carriers) and **loud guardrails** (runtime errors with underscored names that read as scary and
are actually the framework doing you a favor). This phase is the field guide to both.

## The cheat-card

| Symptom / message | Almost always means | Fix |
|---|---|---|
| UI ignores updates, console shows data changing | A dead snapshot: destructured `$state`, or state captured into a plain variable | Read through the object, or `$derived` a live view (phase 2) |
| **`state_unsafe_mutation`** | Writing state during rendering - often from inside a `$derived` or template expression | Derivations must be pure; move writes to handlers/effects |
| **Effect loop / `effect_update_depth_exceeded`** | An effect writes state it also reads | It's a derivation in disguise - use `$derived` (phase 6) |
| **`ownership_invalid_mutation`** warning | Child mutating a prop it doesn't own | Callback prop up, or make it `$bindable` (phase 4) |
| Rows show wrong state after delete/reorder | Unkeyed `{#each}` | `(item.id)` key expression (phase 3) |
| **`Cannot export state...reassigned`** at build | `export let x = $state(...)` in a module | Export an object you mutate, or accessor functions (phase 5) |
| `on:click` vs `onclick` behaving oddly together | Mixed dialects in one component | Pick one per component; runes components use `onclick` |
| Effect never re-fires on a value | Trigger only read inside async callback | Synchronous read at the top of the effect (phase 6) |
| `window is not defined` during `npm run build` | Browser API at SSR time (SvelteKit prerender) | `$effect`/`onMount` for browser-only code; `browser` guard from `$app/environment` |

## The silent one: dead snapshots

The disease you already met in phase 2, now with its common disguises in one place:

```js
let user = $state({ name: 'Ada', theme: 'dark' });

let { theme } = user;                    // disguise 1: destructuring
const settings = { ...user };            // disguise 2: spread copies
localStorage_theme = user.theme;         // disguise 3: parked in a plain variable
setInterval(() => console.log(theme), 1000);  // forever 'dark', whatever the user picks
```

All three copy a value *out* of the proxy at one instant; nothing re-runs that copy later. The
diagnostic question when the screen (or a callback) disagrees with your data: **"is this read
going through the `$state` object right now, or through a copy taken earlier?"** Fixes, in order:
read `user.theme` at the point of use; or declare `let theme = $derived(user.theme)` for a live
alias; or - for the interval case - read inside the callback, since the callback runs fresh each
tick even though tracking doesn't apply there.

Contrast worth naming: in the *markup*, this bug barely exists - `{user.theme}` compiles to a
tracked read. The dead-snapshot family lives almost entirely in `<script>` code and helper
functions.

## The loud one: state_unsafe_mutation

```html
<script>
  let items = $state([]);
  let renderCount = $state(0);

  let sorted = $derived.by(() => {
    renderCount++;                        // ✗ writing state inside a derivation
    return [...items].sort(byName);
  });
</script>
```

```text
Uncaught Svelte error: state_unsafe_mutation
Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden
```

Why so strict: derivations and template expressions may run at any time, any number of times,
whenever the graph recalculates - a write inside one makes the answer depend on how often
questions get asked. Every framework has this rule ("rendering must be pure"); Svelte enforces it
with an error instead of letting the heisenbug ship. The fix is relocation: counters and logging
belong in handlers or effects; the derivation returns its value and touches nothing.

The sibling failure - an `$effect` that writes what it reads - manifests as the loop error, and
phase 6 already gave the verdict: that's a derivation wearing an effect's clothes.

## The migration one: two dialects in one file

Inherited codebases (again: including this site's) mix eras, and a few failure smells are pure
dialect confusion:

- `on:click` in a component that uses runes - works with a deprecation warning, but mixing
  `on:click={a}` and `onclick={b}` on one element invites ordering surprises. One dialect per
  component.
- `export let name` alongside `$props()` - the compiler rejects it; a component is either
  legacy-props or runes-props, never both.
- `$: total = ...` in a runes component - `$:` labels are inert once runes are in play; the "it
  just stopped recalculating" mystery after a partial migration is usually this.

The rule that prevents all of it: **migrate per component, completely.** The dialects interoperate
fine *across* component boundaries; they fight *within* one.

## Reading Svelte's errors

Two habits worth building:

- **The error names are documentation keys.** `state_unsafe_mutation`,
  `ownership_invalid_mutation`, `effect_update_depth_exceeded` - each has a page in the official
  docs explaining cause and fix. Ugly names, excellent lookup.
- **`$inspect` is the debugging rune:** `$inspect(cart)` logs the value now *and on every
  change*, with correct proxy unwrapping - where a stale `console.log` in an effect can itself
  fall into the snapshot traps this phase is about. It compiles away entirely in production
  builds, so it's safe to leave in during development.

## Recap

1. Silent staleness = a copy escaped the proxy: destructure, spread, or parked variable. Ask
   "does this read go through the object *now*?"
2. `state_unsafe_mutation` = a write inside pure territory (derivation/template). Relocate the
   write; keep derivations pure.
3. Effect loops are derivations in disguise; ownership warnings are the props contract enforced.
4. Mixed dialects fail in shapes all their own - migrate each component fully or not at all.
5. Error names are doc keys; `$inspect` beats `console.log` for watching state.

```quiz
[
  {
    "q": "Svelte throws state_unsafe_mutation pointing at a $derived that increments a counter while computing. Why does the framework forbid this outright?",
    "choices": [
      "Writes inside derivations are slow",
      "Derivations run whenever the graph recalculates, any number of times - a write inside makes program state depend on evaluation count",
      "Counters must always live in .svelte.js modules",
      "It's a deprecation: allowed in legacy mode, removed in runes"
    ],
    "answer": 1,
    "why": [
      "Performance isn't the issue - determinism is.",
      null,
      "Where the counter lives doesn't matter; when it's written does.",
      "Legacy $: had the same purity expectations - runes just enforce them with an error."
    ],
    "explain": "A derivation is a pure answer to 'what is this value?' If computing the answer changes other state, the app's behavior depends on how often Svelte happens to recompute - so it's an error, not a footgun."
  },
  {
    "q": "A migrated component keeps a leftover $: fullName = first + ' ' + last, and fullName silently stops updating. What happened?",
    "choices": [
      "String concatenation isn't reactive in Svelte 5",
      "Once a component uses runes, $: labels lose their reactive meaning - the line runs once and never again",
      "first and last needed to be exported",
      "The compiler removed the line as dead code"
    ],
    "answer": 1,
    "why": [
      "Concatenation is fine - inside $derived.",
      null,
      "Exports relate to module state, not local derivations.",
      "The line survives and runs - once, as plain JavaScript, which is the trap."
    ],
    "explain": "Dialects don't mix within a component: in runes mode, $: is just a JavaScript label. Finish the migration - let fullName = $derived(...) - or leave the whole component legacy."
  },
  {
    "q": "Per this phase, when your UI shows stale data but the console proves the state object is correct, what's the first question to ask?",
    "choices": [
      "Is the component missing a key prop?",
      "Is the failing read going through the $state object right now, or through a copy captured earlier?",
      "Should this component be migrated to runes?",
      "Is the effect depth limit being hit?"
    ],
    "answer": 1,
    "why": [
      "Keys matter inside each blocks - but data-right-screen-wrong is the snapshot signature first.",
      null,
      "Dialect issues have their own smells (dead $: lines); this one is about copies.",
      "Depth errors are loud - silence points at a dead snapshot."
    ],
    "explain": "Correct state + stale display means some read isn't going through the proxy anymore. Hunt the destructure, spread, or parked variable on the path between state and screen."
  }
]
```


---

# Where to Go Next

Svelte's ecosystem map is shorter than React's or Vue's, and that's a feature with a reason: more
of what you need ships in the box. State management is runes and modules (phase 5). Scoped styles
are built in. Animation is built in. The one genuinely big next step is **SvelteKit** - and since
your scaffold from phase 1 was already a SvelteKit project, you're closer than you think.

## What you can already build

Phases 1-7 are a complete component-layer skillset. Before adding anything, build two or three real
things: the pains you meet are the only trustworthy tool-selection criteria - the same advice we
give in every framework guide, and it compounds here because Svelte's box already covers so much.

## SvelteKit: the server-in-front decision, Svelte edition

SvelteKit is to Svelte what Next is to React and Nuxt is to Vue: file-based routing, rendering on
a server for first paint and SEO, and a data layer. The *reasoning* for when you need one is
framework-independent - our [Next.js guide's opening phase](../nextjs-from-zero/01-what-nextjs-actually-is.md)
lays out the SPA costs and the server's answer; it transfers here wholesale. What's worth noting
is SvelteKit's own vocabulary, so the docs feel familiar when you arrive:

| Concept | SvelteKit spelling |
|---|---|
| A page | `src/routes/about/+page.svelte` |
| Its server-side data | `+page.server.js` exporting `load()` - the page receives `data` as a prop |
| Layouts | `+layout.svelte`, nested by folder like everything else |
| Form handling | **form actions** in `+page.server.js` - progressively-enhanced POST handlers |
| API endpoints | `+server.js` exporting `GET`/`POST` |
| Static/dynamic/prerender | `export const prerender = true` and friends per route |

Two connect-the-dots from what you know: `load()` is phase 4-6's fetch discipline moved
server-side (the `{#await}` machinery mostly dissolves - data arrives as a prop), and form
actions are the phase-3 form patterns with the server round-trip handled. This site serves every
guide page you've been reading through exactly this machinery - server-rendered, then hydrated.

## The built-in treat: transitions

Most frameworks outsource animation; Svelte ships it, and it's genuinely one of the nicest parts
of the box:

```html
<script>
  import { fade, fly } from 'svelte/transition';
  let visible = $state(true);
</script>

{#if visible}
  <p transition:fly={{ y: 20, duration: 200 }}>Now you see me.</p>
{/if}
```

*What just happened:* the element animates in when the `{#if}` turns true and out when it turns
false - enter and exit both, from one attribute, compiled like everything else. `fade`, `fly`,
`slide`, `scale` cover dailies; `animate:flip` smooths list reorders in keyed each blocks. A
follow-up guide could go deep; for now, know that "animate this appearing" is one directive, not a
library decision.

## The (short) map: pain → tool

| When you feel this | Reach for |
|---|---|
| URLs, SEO, server data, forms | **SvelteKit** - the one big step |
| Ready-made accessible components | **Bits UI / Melt UI** (headless), **Flowbite Svelte**, or **shadcn-svelte** |
| Repetitive fetch caching/refetching | **TanStack Query (Svelte)** - same library, Svelte adapter |
| Legacy store-based code and libraries | The `svelte/store` docs - an afternoon of reading (phase 5's table) |
| Type-checking components | TypeScript - `lang="ts"` and type `$props()`; the compiler's analysis gets even sharper |

What's deliberately absent from this table: a state-management library (runes + modules already
are one) and a CSS-in-JS pick (scoped styles are native). Smaller ecosystem, but also less
ecosystem *required* - a fair trade to weigh against React's larger job market and library
catalog, which remains the strongest argument on the other side.

## Additional resources

- [svelte.dev/docs](https://svelte.dev/docs) - official docs, runes-first; the interactive
  tutorial at [svelte.dev/tutorial](https://svelte.dev/tutorial) is among the best in the industry
  and covers transitions properly.
- [SvelteKit docs](https://svelte.dev/docs/kit) - read "Routing" and "Loading data" first; that's
  80% of daily Kit.
- [Bits UI](https://bits-ui.com) - headless accessible components; a good first dependency when
  you outgrow hand-rolled dialogs.

## Recap

1. SvelteKit is the one big next step - the server-in-front decision with Svelte vocabulary:
   `+page.svelte`, `load()`, form actions.
2. Transitions ship in the box and are worth twenty minutes early - enter/exit animation as a
   directive.
3. The ecosystem is short on purpose: state, styling, and animation are built in; add UI kits and
   a data layer when their pains arrive.
4. TypeScript sharpens an already-compiler-centric workflow - type your `$props()` at minimum.

```quiz
[
  {
    "q": "Coming from this guide, what does SvelteKit's load() function largely replace?",
    "choices": [
      "The $state rune for page-level data",
      "The component-side fetch patterns - onMount fetches and {#await} blocks - by delivering data to the page as a prop, fetched server-side",
      "The need for +page.svelte files",
      "Svelte's compiler"
    ],
    "answer": 1,
    "why": [
      "$state remains the tool for interactive state - load() feeds pages their initial data.",
      null,
      "Pages remain components - load() is the data half beside them.",
      "Kit builds on the same compiler; nothing replaces it."
    ],
    "explain": "load() moves data fetching server-side and hands the result to the page as a prop - the loading/error choreography of client-side fetching mostly dissolves, with SEO and first-paint benefits included."
  },
  {
    "q": "A designer asks for list items to animate in when added and out when removed. In Svelte, what's the realistic effort estimate?",
    "choices": [
      "A day - pick and integrate an animation library",
      "Minutes - transition: directives handle enter/exit, animate:flip smooths reorders, all built in",
      "It requires SvelteKit",
      "Only possible with CSS keyframes written by hand"
    ],
    "answer": 1,
    "why": [
      "That's the estimate in ecosystems where animation is a dependency decision - here it's shipped.",
      null,
      "Transitions are core Svelte - no Kit involved.",
      "The directives generate the CSS for you; hand-rolling remains an option, not a requirement."
    ],
    "explain": "transition:fly / fade on the element inside the keyed each block, animate:flip for reorder smoothing - enter and exit animation is a language feature here, not a library."
  }
]
```
