Svelte logo

Svelte by Example: More Reactivity

Svelte's reactivity can handle longer calculations, immutable updates, and reads that shouldn't trigger updates.

In this example, we'll summarize our todos and keep a copy of the list.

$derived.by accepts a function for calculations that need more than one expression. Keep the calculation free of side effects.

TodoList.svelte
<script>
  let todos = $state([
    { task: 'Walk dog', completed: false },
  ]);

  let remaining = $derived.by(() => {
    let count = 0;
    for (const todo of todos) {
      if (!todo.completed) count++;
    }
    return count;
  });
</script>

{#each todos as todo}
  <label>
    <input type="checkbox" bind:checked={todo.completed}>
    {todo.task}
  </label>
{/each}
<p>{remaining} todos left</p>

Raw State

$state.raw doesn't make objects or arrays deeply reactive. Replace the array instead of mutating it with push.

Reassigning the array updates the list. To change an object inside it, replace that object too.

TodoList.svelte
<script>
  let todos = $state.raw(['Walk dog']);






  function addTodo() {
    todos = [...todos, 'Read newspaper'];
  }
</script>

<button onclick={addTodo}>Add a todo</button>
{#each todos as task, index (index)}
  <p>{task}</p>
{/each}

Snapshots

$state.snapshot takes a static copy of our deeply reactive state, without its proxies. Later edits won't change this copy.

TodoList.svelte
<script>
  let todo = $state({ task: 'Walk dog', completed: false });
  let saved = $state.raw(null);



  function saveCopy() {
    saved = $state.snapshot(todo);
  }
</script>

<input aria-label="Task" bind:value={todo.task}>
<button onclick={saveCopy}>Keep a copy</button>
{#if saved}
  <p>Saved task: {saved.task}</p>
{/if}

Untracked Reads

Sometimes a side effect needs extra information that shouldn't cause it to run again. untrack opts a read out of dependency tracking.

This effect logs when the completed count changes. Editing the list's name alone won't run it again.

untrack still reads the current name each time the effect runs. It doesn't freeze the value.

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

  let name = $state('Weekend');
  let completed = $state(0);

  $effect(() => {
    const count = completed;





    const label = untrack(() => name);
    console.log(`${label}: ${count} completed`);
  });
</script>

<input aria-label="List name" bind:value={name}>
<button onclick={() => completed++}>Complete a todo</button>