svelte-5-runes

Complete guide for Svelte 5 runes ($state, $derived, $effect, $props, $bindable). Use for any Svelte 5 project or when code contains $ prefixed runes. Essential for reactive state management, computed

SKILL.md

GitHub

Svelte 5 Runes

Feature Status: Stable in Svelte 5.0+ (October 2024) Documentation Source: https://svelte.dev/docs/svelte/what-are-runes

Complete guide to Svelte 5's reactivity system using runes.

When to Use This Skill

Use this skill for any Svelte 5 project. Runes are the core reactivity primitives:

  • Creating reactive state with $state
  • Computing derived values with $derived
  • Running side effects with $effect
  • Defining component props with $props
  • Creating bindable props with $bindable
  • Debugging with $inspect

What Are Runes?

Runes are symbols prefixed with $ that control Svelte's compiler. They are:

  • Built-in language keywords - No imports required
  • Not functions - Cannot be assigned to variables or passed around
  • Position-dependent - Only valid in specific contexts

Available Runes:

  • $state - Declare reactive state
  • $derived - Create computed values
  • $effect - Manage side effects
  • $props - Define component properties
  • $bindable - Enable two-way binding on props
  • $inspect - Debug tool for state inspection
  • $host - Access custom element host (web components)
  • $props.id() - Generate consistent component-scoped IDs

Quick Start Patterns

Reactive State ($state)

<script>
    let count = $state(0);
    let user = $state({ name: 'Ada', age: 28 });
</script>

<button onclick={() => count++}>
    Clicks: {count}
</button>

<button onclick={() => user.age++}>
    Happy birthday {user.name}! Age: {user.age}
</button>

Key features:

  • Deep reactivity by default (objects/arrays)
  • Mutations trigger UI updates
  • Use $state.raw() for non-reactive data
  • Works in class fields

Computed Values ($derived)

<script>
    let count = $state(0);
    let doubled = $derived(count * 2);

    // Complex derivations
    let status = $derived.by(() => {
        if (count < 5) return 'low';
        if (count < 10) return 'medium';
        return 'high';
    });
</script>

<p>{count} doubled is {doubled}</p>
<p>Status: {status}</p>

When to use:

  • Transforming state values
  • Computing aggregations
  • Filtering/mapping arrays
  • ANY value derived from other state

Side Effects ($effect)

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

    $effect(() => {
        // Runs when count changes
        console.log('Count changed:', count);

        // Cleanup function (optional)
        return () => {
            console.log('Cleanup before re-run');
        };
    });

    $effect(() => {
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, 100, 100);
        ctx.fillRect(0, 0, count, count);
    });
</script>

<canvas bind:this={canvas} width="100" height="100"></canvas>

Use for:

  • Canvas drawing
  • Third-party library integration
  • Analytics tracking
  • Network requests (with caution)

DON'T use for:

  • Synchronizing state (use $derived)
  • Computing values (use $derived)

Component Props ($props)

<!-- Parent.svelte -->
<script>
    import Child from './Child.svelte';
</script>

<Child name="Ada" age={28} />
<!-- Child.svelte -->
<script>
    // Destructure with defaults
    let { name, age = 18 } = $props();

    // Or get all props
    let props = $props();
</script>

<p>{name} is {age} years old</p>

TypeScript:

<script lang="ts">
    interface Props {
        name: string;
        age?: number;
    }

    let { name, age = 18 }: Props = $props();
</script>

Two-Way Binding ($bindable)

<!-- TextInput.svelte -->
<script>
    let { value = $bindable(''), placeholder } = $props();
</script>

<input bind:value {placeholder} />
<!-- App.svelte -->
<script>
    import TextInput from './TextInput.svelte';

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

<TextInput bind:value={message} placeholder="Type here" />
<p>You typed: {message}</p>

Common Workflows

Form with Validation

<script>
    let email = $state('');
    let password = $state('');

    let isValidEmail = $derived(email.includes('@') && email.includes('.'));
    let isValidPassword = $derived(password.length >= 8);
    let canSubmit = $derived(isValidEmail && isValidPassword);

    function handleSubmit() {
        if (!canSubmit) return;
        // Submit form
    }
</script>

<form onsubmit={handleSubmit}>
    <input bind:value={email} type="email" />
    {#if email && !isValidEmail}
        <p class="error">Invalid email</p>
    {/if}

    <input bind:value={password} type="password" />
    {#if password && !isValidPassword}
        <p class="error">Password must be 8+ characters</p>
    {/if}

    <button disabled={!canSubmit}>Submit</button>
</form>

Data Fetching with Loading States

<script>
    let userId = $state(1);
    let userData = $state(null);
    let loading = $state(false);
    let error = $state(null);

    $effect(() => {
        // Tracks userId - refetches when it changes
        loading = true;
        error = null;

        fetch(`/api/users/${userId}`)
            .then(r => r.json())
            .then(data => {
                userData = data;
                loading = false;
            })
            .catch(e => {
                error = e.message;
                loading = false;
            });
    });
</script>

{#if loading}
    <p>Loading...</p>
{:else if error}
    <p class="error">{error}</p>
{:else if userData}
    <div>
        <h2>{userData.name}</h2>
        <p>{userData.email}</p>
    </div>
{/if}

<button onclick={() => userId++}>Next User</button>

Class-Based State

<script>
    class Counter {
        count = $state(0);

        increment = () => {
            this.count++;
        }

        reset() {
            this.count = 0;
        }
    }

    let counter = new Counter();
</script>

<button onclick={counter.increment}>
    Count: {counter.count}
</button>
<button onclick={() => counter.reset()}>Reset</button>

Best Practices

1. Use $derived, Not $effect for Computed Values

❌ Don't do this:

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

    $effect(() => {
        doubled = count * 2;
    });
</script>

✅ Do this:

<script>
    let count = $state(0);
    let doubled = $derived(count * 2);
</script>

2. Understand Deep Reactivity

<script>
    let items = $state([1, 2, 3]);

    // All these trigger reactivity:
    items.push(4);          // ✅ Works
    items[0] = 10;          // ✅ Works
    items = items.filter(x => x > 1); // ✅ Works

    // Destructuring breaks reactivity:
    let [first] = items;
    items[0] = 99;          // first still has old value
</script>

3. Use $state.raw for Large Immutable Data

<script>
    // Don't need deep reactivity - performance win
    let config = $state.raw({
        apiUrl: 'https://api.example.com',
        timeout: 5000,
        // ... hundreds of properties
    });

    // Only reassign, don't mutate
    config = { ...config, timeout: 10000 };
</script>

4. Effect Dependencies Are Automatic

<script>
    let a = $state(1);
    let b = $state(2);
    let c = $state(3);

    $effect(() => {
        // Only depends on `a` and `b`
        console.log(a + b);

        // `c` is not a dependency - won't rerun when c changes
    });
</script>

5. Props Flow Down, Events Flow Up

<!-- Child.svelte -->
<script>
    let { count, onIncrement } = $props();
</script>

<button onclick={onIncrement}>
    Count: {count}
</button>
<!-- Parent.svelte -->
<script>
    import Child from './Child.svelte';

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

<Child {count} onIncrement={() => count++} />

6. Don't Mutate Props (Use $bindable)

❌ Don't:

<script>
    let { count } = $props();
</script>

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

✅ Do:

<script>
    let { count = $bindable() } = $props();
</script>

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

7. Use $inspect for Debugging

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

    $inspect(count); // Logs when count changes with stack trace

    // Custom logging
    $inspect(count).with((type, value) => {
        if (type === 'update') {
            console.log('Count updated to:', value);
        }
    });
</script>

Common Pitfalls

Async State in Effects

Issue: Async code doesn't track dependencies.

<script>
    let userId = $state(1);
    let user = $state(null);

    $effect(() => {
        // userId IS tracked (read before await)
        const id = userId;

        fetch(`/api/users/${id}`)
            .then(r => r.json())
            .then(data => {
                // This works, but any state read HERE
                // is NOT tracked as a dependency
                user = data;
            });
    });
</script>

Effect Cleanup

Always cleanup subscriptions:

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

    $effect(() => {
        const interval = setInterval(() => {
            count++;
        }, 1000);

        // Cleanup when effect re-runs or component unmounts
        return () => clearInterval(interval);
    });
</script>

Infinite Loops

Don't read and write same state in effect:

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

    // ❌ Infinite loop!
    $effect(() => {
        count = count + 1;
    });

    // ✅ Use derived instead
    let incremented = $derived(count + 1);
</script>

Migration from Svelte 4

Reactive Declarations

Svelte 4:

<script>
    let count = 0;
    $: doubled = count * 2;
    $: console.log('Count changed:', count);
</script>

Svelte 5:

<script>
    let count = $state(0);
    let doubled = $derived(count * 2);

    $effect(() => {
        console.log('Count changed:', count);
    });
</script>

Component Props

Svelte 4:

<script>
    export let name;
    export let age = 18;
</script>

Svelte 5:

<script>
    let { name, age = 18 } = $props();
</script>

Two-Way Binding

Svelte 4:

<script>
    export let value;
</script>

<input bind:value />

Svelte 5:

<script>
    let { value = $bindable() } = $props();
</script>

<input bind:value />

Detailed Documentation

For complete implementation details, read the reference files:

  • references/quick-reference.md - All runes syntax at a glance
  • references/state.md - Complete $state documentation (deep reactivity, classes, raw state)
  • references/derived.md - $derived patterns and dependencies
  • references/effect.md - $effect lifecycle, cleanup, and when NOT to use
  • references/props.md - $props type safety and patterns
  • references/other-runes.md - $bindable, $inspect, $host usage

Important Notes

  • Runes are compile-time features - no runtime imports
  • Effects run after DOM updates (use $effect.pre for before)
  • Dependencies are tracked synchronously - async code won't track
  • $state creates deep reactive proxies for objects/arrays
  • Can't export $state directly from .svelte.js if reassigned
  • Props should not be mutated unless marked with $bindable