docs(codestyle): add Svelte 5 specific rules with examples
Document the three key rules enforced by eslint-plugin-svelte: - svelte/require-each-key: why position-based tracking silently corrupts state - svelte/prefer-writable-derived: why $state+$effect is wrong for computed values - svelte/prefer-svelte-reactivity: why SvelteMap/SvelteURLSearchParams are needed Each rule includes bad/good code examples and a technical reason. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
68
CODESTYLE.md
68
CODESTYLE.md
@@ -259,3 +259,71 @@ These complement the principles above with project-specific conventions.
|
|||||||
- Check `!result.response.ok` for API errors, not `result.error` (see CLAUDE.md).
|
- Check `!result.response.ok` for API errors, not `result.error` (see CLAUDE.md).
|
||||||
- Prefer typed API client calls over raw `fetch` — use raw `fetch` only for multipart uploads.
|
- Prefer typed API client calls over raw `fetch` — use raw `fetch` only for multipart uploads.
|
||||||
- Svelte component logic in `<script>`, layout/styles in template — no business logic in markup.
|
- Svelte component logic in `<script>`, layout/styles in template — no business logic in markup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Svelte 5 — Specific Rules
|
||||||
|
|
||||||
|
These rules are enforced by ESLint (`eslint-plugin-svelte`). Knowing *why* they exist prevents the need to fix violations after the fact.
|
||||||
|
|
||||||
|
### Always key `{#each}` blocks
|
||||||
|
|
||||||
|
Without a key, Svelte tracks list items by array position. When items are added, removed, or reordered, Svelte patches DOM nodes in-place from the top — it never moves the correct node. Component-local state (counters, animation state, focus) becomes permanently attached to the wrong item. This is a silent data integrity bug, not a crash.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- Bad — position-based tracking; reordering silently corrupts local state -->
|
||||||
|
{#each documents as doc}
|
||||||
|
<DocumentCard {doc} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- Good — identity-based; each node follows its data through reorders -->
|
||||||
|
{#each documents as doc (doc.id)}
|
||||||
|
<DocumentCard {doc} />
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `(item.id)` when items have a stable ID. Use the loop index `(i)` only for static lists that will never be reordered. Use `(item)` for primitive lists.
|
||||||
|
|
||||||
|
### Use `$derived` for computed values, never `$state` + `$effect`
|
||||||
|
|
||||||
|
`$effect` is for *side effects* (DOM calls, network, logging). Using it to assign a computed value introduces a timing problem: `$derived` updates synchronously before the render, while `$effect` runs *after* the render — meaning the component briefly displays a stale value. It also triggers a second reactive pass, doubling the work.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- Bad — stale value during render; extra reactive cycle; unclear intent -->
|
||||||
|
<script>
|
||||||
|
let fullName = $state('');
|
||||||
|
$effect(() => {
|
||||||
|
fullName = `${person.firstName} ${person.lastName}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Good — synchronous, single-pass, intent is obvious -->
|
||||||
|
<script>
|
||||||
|
const fullName = $derived(`${person.firstName} ${person.lastName}`);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `$derived.by(() => { ... })` when the computation needs multiple statements.
|
||||||
|
|
||||||
|
### Use Svelte reactive collections, not plain JS ones
|
||||||
|
|
||||||
|
Svelte 5's reactivity tracks object *references*, not mutations. When you call `.set()` on a plain `Map` or `.set()` on a plain `URLSearchParams`, the reference doesn't change — Svelte never notices, and the UI goes silently stale.
|
||||||
|
|
||||||
|
`SvelteMap`, `SvelteSet`, and `SvelteURLSearchParams` from `svelte/reactivity` wrap the native classes and hook into Svelte's dependency tracker. Every mutation notifies the reactive graph; every read registers a dependency.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- Bad — mutations are invisible to Svelte; derived values never update -->
|
||||||
|
<script>
|
||||||
|
const freq = new Map<string, number>();
|
||||||
|
freq.set('key', 1); // Svelte does not see this
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Good — mutations are tracked; all dependents re-run correctly -->
|
||||||
|
<script>
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
const freq = new SvelteMap<string, number>();
|
||||||
|
freq.set('key', 1); // Svelte tracks this
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The same applies to `URLSearchParams` in reactive contexts — use `SvelteURLSearchParams`.
|
||||||
|
|||||||
Reference in New Issue
Block a user