Svelte
01 / 07

Reactivity & Stores

Svelte Reactivity & Stores

Svelte is a radical new approach to building user interfaces. It compiles components into highly efficient imperative code that updates the DOM surgically. Reactivity in Svelte is built into the language itself.

Reactive Declarations

Svelte automatically tracks dependencies and updates the DOM when state changes. Use the $ label for reactive declarations.

<script>
  // Regular variable - reactive by assignment
  let count = 0;
  let name = 'Svelte';
  
  // Reactive declaration - auto-updates when dependencies change
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;
  
  // Reactive statement - runs when dependencies change
  $: {
    console.log('Count changed to:', count);
    console.log('Doubled:', doubled);
  }
  
  // Conditional reactive statement
  $: if (count > 10) {
    console.log('Count is greater than 10!');
  }
  
  function increment() {
    count += 1;  // Assignment triggers reactivity
  }
  
  function reset() {
    count = 0;
  }
</script>

<h1>{name}</h1>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Quadrupled: {quadrupled}</p>

<button on:click={increment}>+1</button>
<button on:click={reset}>Reset</button>

Arrays and Objects Reactivity

<script>
  let todos = [
    { id: 1, text: 'Learn Svelte', done: false },
    { id: 2, text: 'Build app', done: false }
  ];
  
  let user = {
    name: 'John',
    email: 'john@example.com',
    age: 30
  };
  
  // ✅ Triggers reactivity - reassignment
  function addTodo() {
    todos = [...todos, { id: Date.now(), text: 'New todo', done: false }];
  }
  
  // ✅ Triggers reactivity - reassignment
  function updateUser() {
    user = { ...user, age: 31 };
  }
  
  // ❌ Won't trigger reactivity - mutation
  function addTodoWrong() {
    todos.push({ id: Date.now(), text: 'New', done: false });
    // Need to reassign: todos = todos
  }
  
  // ✅ Fix: Reassign after mutation
  function addTodoCorrect() {
    todos.push({ id: Date.now(), text: 'New', done: false });
    todos = todos;  // Trigger reactivity
  }
  
  // ✅ Better: Use spread
  function toggleTodo(id) {
    todos = todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    );
  }
  
  // Array methods that need reassignment:
  // push, pop, shift, unshift, splice, sort, reverse
</script>

<ul>
  {#each todos as todo (todo.id)}
    <li>
      <input type="checkbox" checked={todo.done} on:change={() => toggleTodo(todo.id)} />
      {todo.text}
    </li>
  {/each}
</ul>

<button on:click={addTodo}>Add Todo</button>

Stores - Shared State

Svelte stores provide a way to share state across components without prop drilling.

Writable Stores

// stores.js
import { writable } from 'svelte/store';

// Create writable store with initial value
export const count = writable(0);
export const user = writable(null);
export const todos = writable([]);

// Store with custom logic
function createCounter() {
  const { subscribe, set, update } = writable(0);
  
  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0)
  };
}

export const counter = createCounter();

// Component.svelte
<script>
  import { count, user, counter } from './stores';
  
  // Method 1: Subscribe manually
  let countValue;
  const unsubscribe = count.subscribe(value => {
    countValue = value;
  });
  
  // Cleanup
  import { onDestroy } from 'svelte';
  onDestroy(unsubscribe);
  
  // Method 2: Auto-subscribe with $
  // $count is automatically reactive and auto-unsubscribes
  
  function increment() {
    count.update(n => n + 1);
    // Or: count.set($count + 1);
  }
  
  function setUser() {
    user.set({ name: 'John', email: 'john@example.com' });
  }
</script>

<h1>Count: {$count}</h1>
<p>User: {$user?.name || 'None'}</p>

<button on:click={increment}>Increment</button>
<button on:click={() => counter.increment()}>Counter Increment</button>
<button on:click={setUser}>Set User</button>

Readable Stores

import { readable } from 'svelte/store';

// Readable store - cannot be set from outside
export const time = readable(new Date(), set => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);
  
  // Cleanup function (called when last subscriber unsubscribes)
  return () => clearInterval(interval);
});

// Mouse position store
export const mouse = readable({ x: 0, y: 0 }, set => {
  const handleMove = (event) => {
    set({ x: event.clientX, y: event.clientY });
  };
  
  document.addEventListener('mousemove', handleMove);
  
  return () => {
    document.removeEventListener('mousemove', handleMove);
  };
});

// Usage
<script>
  import { time, mouse } from './stores';
</script>

<p>Time: {$time.toLocaleTimeString()}</p>
<p>Mouse: {$mouse.x}, {$mouse.y}</p>

Derived Stores

import { derived, writable } from 'svelte/store';

export const todos = writable([
  { id: 1, text: 'Todo 1', done: false },
  { id: 2, text: 'Todo 2', done: true },
  { id: 3, text: 'Todo 3', done: false }
]);

// Derived from single store
export const completedTodos = derived(
  todos,
  $todos => $todos.filter(t => t.done)
);

export const activeTodos = derived(
  todos,
  $todos => $todos.filter(t => !t.done)
);

export const todoStats = derived(
  todos,
  $todos => ({
    total: $todos.length,
    completed: $todos.filter(t => t.done).length,
    active: $todos.filter(t => !t.done).length
  })
);

// Derived from multiple stores
const firstName = writable('John');
const lastName = writable('Doe');

export const fullName = derived(
  [firstName, lastName],
  ([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);

// Usage
<script>
  import { completedTodos, activeTodos, todoStats } from './stores';
</script>

<p>Total: {$todoStats.total}</p>
<p>Completed: {$todoStats.completed}</p>
<p>Active: {$todoStats.active}</p>

<h3>Completed Todos:</h3>
{#each $completedTodos as todo}
  <p>{todo.text}</p>
{/each}

Custom Stores

// stores/auth.js
import { writable } from 'svelte/store';

function createAuthStore() {
  const { subscribe, set, update } = writable({
    user: null,
    token: null,
    isAuthenticated: false
  });
  
  return {
    subscribe,
    login: async (credentials) => {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials)
      });
      const data = await response.json();
      set({
        user: data.user,
        token: data.token,
        isAuthenticated: true
      });
    },
    logout: () => {
      set({
        user: null,
        token: null,
        isAuthenticated: false
      });
    },
    updateUser: (userData) => {
      update(state => ({
        ...state,
        user: { ...state.user, ...userData }
      }));
    }
  };
}

export const auth = createAuthStore();

// Usage
<script>
  import { auth } from './stores/auth';
  
  async function handleLogin() {
    await auth.login({ email, password });
  }
</script>

{#if $auth.isAuthenticated}
  <p>Welcome, {$auth.user.name}!</p>
  <button on:click={() => auth.logout()}>Logout</button>
{:else}
  <button on:click={handleLogin}>Login</button>
{/if}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free