Svelte 5 Runes in Practice: $state, $derived, $effect, and Migration from Svelte 4

前端工程

Why Svelte 5 Deserves Your Attention

Svelte 5 rewrote its reactivity system with Runes — moving from "compile-time dependency tracking" to "explicit, fine-grained, cross-file" reactive primitives. It fixes the Svelte 4 problem where let variables lost tracking inside closures, async code, and shared modules, while shipping a smaller runtime and better performance.

Dimension Svelte 4 Svelte 5 (Runes)
State let count = 0 let count = $state(0)
Derived $: doubled = count * 2 let doubled = $derived(count * 2)
Side effect $: console.log(count) $effect(() => console.log(count))
Cross-module needs stores runes directly in .svelte.js
Props export let let { x } = $props()

Core Runes at a Glance

$state: declare reactive state

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

  function inc() {
    count++;                 // triggers update automatically
    user.age++;              // deep props are reactive too (proxy-based)
  }
</script>

<button onclick={inc}>{count} - {user.age}</button>

Note: $state uses a deep proxy for objects and arrays, so user.age++ is precisely tracked — no more "replace the whole object" pattern from Svelte 4.

$derived: declare derived values

<script>
  let price = $state(100);
  let qty = $state(2);
  let total = $derived(price * qty);     // recomputed only when deps change
  let label = $derived.by(() => {
    // wrap complex logic with .by
    return price > 0 ? `$${price * qty}` : "invalid price";
  });
</script>

<p>Total: {total} / {label}</p>

$effect: declare side effects

<script>
  let count = $state(0);
  $effect(() => {
    // reading count creates the dependency; re-runs when it changes
    document.title = `Count: ${count}`;
  });
</script>

$effect runs only in the browser — ideal for DOM sync, third-party bindings, and subscription cleanup. Return a function to clean up:

<script>
  let el = $state();
  $effect(() => {
    if (!el) return;
    const obs = new ResizeObserver(() => {/* ... */});
    obs.observe(el);
    return () => obs.disconnect();   // auto-cleanup
  });
</script>

<div bind:this={el}></div>

Props and Two-Way Binding

$props replaces export let

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

<h2>{title}: {count}</h2>

$bindable for bindable props

<!-- Child.svelte -->
<script>
  let { value = $bindable("") } = $props();
</script>

<input bind:value />

<!-- Parent.svelte -->
<script>
  let name = $state("");
</script>

<Child bind:value={name} />
<p>Parent: {name}</p>

Reusing Runes in .svelte.js Modules

A major Svelte 5 win: runes are no longer confined to .svelte files — declare reactive state in .svelte.js / .svelte.ts for true cross-component reuse.

// counter.svelte.js
export const createCounter = () => {
  let count = $state(0);
  const inc = () => count++;
  const double = $derived(count * 2);
  return { get count() { return count; }, get double() { return double; }, inc };
};
<script>
  import { createCounter } from "./counter.svelte.js";
  const c = createCounter();
</script>

<button onclick={c.inc}>{c.count} / {c.double}</button>

For component state with complex config (theme colors, contrast thresholds), organize the config object with the JSON Formatter tool first, then wrap it as a reactive store with runes.


Lifecycle Changes

Svelte 4 Svelte 5 Notes
onMount onMount still works once on mount
onDestroy $effect cleanup return replaces destroy cleanup
$: side effect $effect unified reactive side-effect entry
beforeUpdate/afterUpdate $effect.pre / $effect finer control
<script>
  import { onMount } from "svelte";
  let ready = $state(false);
  onMount(() => { ready = true; });   // mount logic still works
</script>

Smooth Migration from Svelte 4

Svelte 5 supports most Svelte 4 syntax (legacy mode), but migrate step by step:

  1. Upgrade deps: npm i svelte@5, then npx sv migrate svelte-5.
  2. Migrate state first: change let x = to $state(), especially variables used in closures/async.
  3. Then derived: $: y = f(x)let y = $derived(f(x)).
  4. Then effects: $: sideEffect(x)$effect(() => sideEffect(x)).
  5. Finally props: export let$props(), add $bindable where two-way is needed.
<!-- Before (Svelte 4) -->
<script>
  export let value = 0;
  $: doubled = value * 2;
  $: console.log(doubled);
</script>

<!-- After (Svelte 5) -->
<script>
  let { value = 0 } = $props();
  let doubled = $derived(value * 2);
  $effect(() => console.log(doubled));
</script>

For visual config like colors and contrast, verify theme accessibility (WCAG) with the Color Converter and Color Contrast tools.


FAQ

Q1: Can runes be used in a plain .js file?

No. They must be in .svelte, .svelte.js, or .svelte.ts files so the compiler recognizes them.

Q2: Do non-reactive variable changes trigger $effect?

No. $effect only tracks reactive state read during execution. Reading a plain variable creates no dependency.

Q3: Can Svelte 4 code run directly on Svelte 5?

Mostly yes (legacy compatibility). But don't mix runes and legacy syntax within the same component — migrate component by component.

Q4: $derived vs $effect?

Use $derived for derived values; use $effect for doing things (DOM, subscriptions, logs).

Q5: Where does the performance gain come from?

Fine-grained tracking + compile-time optimization avoid virtual DOM diff overhead; state updates hit the minimal DOM nodes directly.


For Svelte 5 development, these ToolsKu tools help:


Svelte 5 runes are not syntactic sugar — they upgrade the mental model of reactivity: explicit state, precise dependencies, cross-file reuse. Master $state / $derived / $effect / $props and you hold the key to the next generation of Svelte.

Try these browser-local tools — no sign-up required →

#Svelte#Svelte 5#Runes#前端框架#响应式#前端