chore: add rule 57 — Svelte 5 reactivity safety (prevent infinite loops)
This commit is contained in:
parent
20e4d2cdec
commit
aaeee808c3
1 changed files with 81 additions and 0 deletions
81
.claude/rules/57-svelte5-reactivity.md
Normal file
81
.claude/rules/57-svelte5-reactivity.md
Normal file
|
|
@ -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
|
||||
<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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue