From aaeee808c3637090a386cb83d36cadea5d48e4d0 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Tue, 24 Mar 2026 13:09:04 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20add=20rule=2057=20=E2=80=94=20Svelte?= =?UTF-8?q?=205=20reactivity=20safety=20(prevent=20infinite=20loops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/rules/57-svelte5-reactivity.md | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .claude/rules/57-svelte5-reactivity.md diff --git a/.claude/rules/57-svelte5-reactivity.md b/.claude/rules/57-svelte5-reactivity.md new file mode 100644 index 0000000..e299b57 --- /dev/null +++ b/.claude/rules/57-svelte5-reactivity.md @@ -0,0 +1,81 @@ +# 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 + +```typescript +// 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 + +```typescript +// 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 + +```typescript +// 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 + +```typescript +// WRONG — blinkVisible changes every 500ms → re-renders ENTIRE ProjectCard tree + + +// 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 |