Skip to main content
Technology & EngineeringSvelte238 lines

Reactivity

Svelte 5 runes system for fine-grained reactivity with $state, $derived, and $effect

Quick Summary16 lines
You are an expert in Svelte 5's runes-based reactivity system, helping developers build performant reactive UIs using $state, $derived, $effect, and related primitives.

## Key Points

- **Infinite `$effect` Loops** — reading and writing the same `$state` variable inside a single `$effect` without using `untrack`, causing the effect to trigger itself recursively.
- **Use `$state.raw` for large collections** that are replaced wholesale rather than mutated, avoiding the overhead of deep proxying.
- **Return cleanup functions from `$effect`** whenever you create subscriptions, timers, or abort controllers.
- **Keep effects focused.** A single `$effect` should do one thing. Split unrelated side effects into separate `$effect` calls.
- **Use `.svelte.js`/`.svelte.ts` extensions** for any module file that uses runes outside a component.
- **Destructuring loses reactivity.** `let { name } = $state({ name: 'A' })` makes `name` a plain string. Keep the object reference intact or use `$derived` on the property.
- **`$effect` runs after mount, not during SSR.** Do not rely on `$effect` for server-side logic; it only executes in the browser.
- **Infinite loops with `$effect`.** Writing to a reactive variable that the same `$effect` reads causes an infinite re-run. Use `untrack` or restructure the logic.
- **Forgetting `.svelte.js` extension.** Runes in a plain `.js` file will not be compiled — the file must use `.svelte.js` or `.svelte.ts`.
- **Comparing `$state` objects by reference.** Deep-reactive state is wrapped in a proxy, so `===` comparison against the original object will fail. Compare by value or use `$state.snapshot()`.
skilldb get svelte-skills/ReactivityFull skill: 238 lines
Paste into your CLAUDE.md or agent config

Reactivity — Svelte 5 Runes

You are an expert in Svelte 5's runes-based reactivity system, helping developers build performant reactive UIs using $state, $derived, $effect, and related primitives.

Core Philosophy

Svelte 5's runes represent a fundamental shift in how Svelte handles reactivity: from implicit compiler magic to explicit, composable primitives. In earlier Svelte versions, let x = 0 was magically reactive inside components, but this magic did not extend to external modules, classes, or complex state shapes. Runes fix this by making reactivity a deliberate choice — $state, $derived, $effect — that works uniformly in components, .svelte.js modules, and class bodies.

The hierarchy of runes reflects a clear design philosophy: state is the foundation, derivations build on state, and effects are the escape hatch for the outside world. $state declares what can change. $derived declares what is computed from state without side effects. $effect is reserved for things that interact with the world outside the reactive graph — DOM manipulation, network calls, logging. This hierarchy is not arbitrary; it maps directly to the functional reactive programming principle that pure derivations should vastly outnumber side effects.

Deep reactivity in $state is the default because it matches how developers think about data. Pushing an item to an array should trigger an update. Changing a nested property should trigger an update. When deep reactivity is too expensive, $state.raw offers an opt-out for large datasets that are replaced wholesale. This opt-in simplicity, opt-out performance model means the common case is easy and the advanced case is possible.

Anti-Patterns

  • Using $effect for Derived Values — writing to a $state variable inside an $effect to compute a value that could be expressed as $derived. This creates unnecessary re-render cycles and makes the dependency graph harder to trace.

  • Destructuring $state Objects — writing let { name } = $state({ name: 'A' }) expecting name to be reactive, when destructuring extracts a plain value from the proxy. Keep the object reference intact or use $derived on individual properties.

  • Infinite $effect Loops — reading and writing the same $state variable inside a single $effect without using untrack, causing the effect to trigger itself recursively.

  • Runes in Plain .js Files — using $state, $derived, or $effect in a file with a .js or .ts extension instead of .svelte.js or .svelte.ts. The Svelte compiler does not process plain JavaScript files, so runes are treated as undefined variables.

  • Reference Comparisons on $state Proxies — comparing $state objects with === against the original object, which fails because $state wraps values in a deep reactive proxy. Use $state.snapshot() or compare by value instead.

Overview

Svelte 5 replaces the implicit reactivity model of earlier versions with explicit runes — compiler-understood signals that give developers fine-grained control over reactive state, derived computations, and side effects. Runes work in .svelte files and in .svelte.js/.svelte.ts modules.

Core Concepts

$state — Reactive State

Declares a piece of reactive state. The compiler tracks reads and writes to trigger updates.

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

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

Deep reactivity: objects and arrays declared with $state are deeply reactive — mutating nested properties triggers updates automatically.

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

  function toggle(index) {
    todos[index].done = !todos[index].done; // triggers update
  }
</script>

$state.raw — Shallow State

When deep reactivity is unnecessary or expensive, use $state.raw for reference-equality-only tracking.

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

  function replace() {
    // Must reassign — mutations are NOT tracked
    items = [...items, 4];
  }
</script>

$derived — Computed Values

Derives a value from other reactive state. Re-evaluates only when its dependencies change.

<script>
  let width = $state(10);
  let height = $state(20);
  let area = $derived(width * height);
</script>

<p>Area: {area}</p>

$derived.by — Complex Derivations

For derivations that need a function body with intermediate variables or logic.

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

  let stats = $derived.by(() => {
    const sorted = [...items].sort((a, b) => a - b);
    return {
      min: sorted[0],
      max: sorted[sorted.length - 1],
      sum: sorted.reduce((a, b) => a + b, 0)
    };
  });
</script>

$effect — Side Effects

Runs a function when its reactive dependencies change. Automatically cleans up on re-run and on destroy.

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

  $effect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => { /* update state */ });

    return () => controller.abort(); // cleanup
  });
</script>

$effect.pre — Pre-DOM-Update Effects

Runs before the DOM updates, useful for measuring or preparing layout work.

<script>
  let messages = $state([]);
  let container;

  $effect.pre(() => {
    // Check scroll position before DOM updates with new messages
    messages.length; // track dependency
    // ... measurement logic
  });
</script>

Implementation Patterns

Reactive Classes

Runes work inside class bodies, making it easy to build reusable reactive models.

// counter.svelte.js
export class Counter {
  count = $state(0);
  doubled = $derived(this.count * 2);

  increment() {
    this.count++;
  }

  reset() {
    this.count = 0;
  }
}
<script>
  import { Counter } from './counter.svelte.js';
  const counter = new Counter();
</script>

<button onclick={() => counter.increment()}>
  {counter.count} (doubled: {counter.doubled})
</button>

Shared Reactive State Across Components

Extract state into .svelte.js modules for cross-component sharing.

// cart.svelte.js
let items = $state([]);

export function addItem(item) {
  items.push(item);
}

export function getItems() {
  return items;
}

export const totalPrice = $derived(
  items.reduce((sum, i) => sum + i.price, 0)
);

Untracking Dependencies

Use untrack to read reactive state without creating a dependency.

<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let log = $state([]);

  $effect(() => {
    // Only re-run when count changes, not when log changes
    const current = count;
    const prevLog = untrack(() => log);
    log = [...prevLog, `count is ${current}`];
  });
</script>

Best Practices

  • Prefer $derived over $effect for computed values. If the goal is to compute a new value from state, use $derived. Reserve $effect for true side effects (fetching, logging, DOM manipulation).
  • Use $state.raw for large collections that are replaced wholesale rather than mutated, avoiding the overhead of deep proxying.
  • Return cleanup functions from $effect whenever you create subscriptions, timers, or abort controllers.
  • Keep effects focused. A single $effect should do one thing. Split unrelated side effects into separate $effect calls.
  • Use .svelte.js/.svelte.ts extensions for any module file that uses runes outside a component.

Common Pitfalls

  • Destructuring loses reactivity. let { name } = $state({ name: 'A' }) makes name a plain string. Keep the object reference intact or use $derived on the property.
  • $effect runs after mount, not during SSR. Do not rely on $effect for server-side logic; it only executes in the browser.
  • Infinite loops with $effect. Writing to a reactive variable that the same $effect reads causes an infinite re-run. Use untrack or restructure the logic.
  • Forgetting .svelte.js extension. Runes in a plain .js file will not be compiled — the file must use .svelte.js or .svelte.ts.
  • Comparing $state objects by reference. Deep-reactive state is wrapped in a proxy, so === comparison against the original object will fail. Compare by value or use $state.snapshot().

Install this skill directly: skilldb add svelte-skills

Get CLI access →