2.7 KiB
2.7 KiB
Svelte 5 Reactivity Safety (PARAMOUNT)
Svelte 5's reactive system triggers re-evaluation when it detects a new reference. Misuse causes infinite loops that pin CPU at 100%+.
Rules
$derived — NEVER create new objects or arrays
// WRONG — .filter()/.map()/.reduce()/??[] create new references every evaluation
let messages = $derived(session?.messages ?? []); // new [] each time
let fileRefs = $derived(messages.filter(m => m.toolPath)); // new array each time
let grouped = $derived(items.reduce((a, i) => ..., {})); // new object each time
// RIGHT — plain getter functions called in template
function getMessages() { return session?.messages ?? EMPTY; }
function getFileRefs() { return getMessages().filter(m => m.toolPath); }
$derived is safe ONLY for primitives (number, string, boolean) or stable references (reading a field from a $state object).
$effect — NEVER write to $state that the effect reads
// WRONG — reads openDirs, writes openDirs → infinite loop
$effect(() => {
openDirs = new Set(openDirs); // read + write same $state
});
// WRONG — calls function that writes to $state
$effect(() => {
loadTasks(); // internally does ++pollToken which is $state
});
// RIGHT — use onMount for initialization
onMount(() => {
loadTasks();
openDirs = new Set([cwd]);
});
$effect — NEVER use for async initialization
// WRONG — async writes to $state during effect evaluation
$effect(() => {
loadData(); // writes to channels, agents, etc.
});
// RIGHT — onMount for all async init + side effects
onMount(() => {
loadData();
const timer = setInterval(poll, 30000);
return () => clearInterval(timer);
});
Props that change frequently — NEVER pass as component props
// WRONG — blinkVisible changes every 500ms → re-renders ENTIRE ProjectCard tree
<ProjectCard {blinkVisible} />
// RIGHT — store-based, only the leaf component (StatusDot) reads it
// blink-store.svelte.ts owns the timer
// StatusDot imports getBlinkVisible() directly
Quick Reference
| Pattern | Safe? | Why |
|---|---|---|
$derived(count + 1) |
Yes | Primitive, stable |
$derived(obj.field) |
Yes | Stable reference |
$derived(arr.filter(...)) |
NO | New array every eval |
$derived(x ?? []) |
NO | New [] when x is undefined |
$derived(items.map(...)) |
NO | New array + new objects |
$effect(() => { state = ... }) |
NO | Write triggers re-run |
$effect(() => { asyncFn() }) |
NO | Async writes to state |
onMount(() => { state = ... }) |
Yes | Runs once, no re-trigger |
| Plain function in template | Yes | Svelte tracks inner reads |