Anchor for Svelte - AI Knowledge Base
Reference for AI assistants to build robust Svelte applications using Anchor.
Core Concepts
What is Anchor for Svelte?
Anchor is a state management library for Svelte 5 that provides:
- Direct Mutation: Mutate state directly while maintaining reactivity (like runes)
- Schema Validation: Runtime validation with Zod for data integrity
- Async State Management: Built-in
query(),fetchState(),streamState()with status tracking - Form Handling:
form()with automatic validation and error tracking - Immutability Patterns:
immutable()+writable()for controlled shared state access - Advanced Patterns:
persistent(),session(),undoable(),snapshot() - Portability: Business logic written with
@anchorlib/coreis framework-agnostic and can be shared across React, SolidJS, Svelte, or vanilla JS
While Svelte 5 runes ($state()) provide direct mutation for reactive state, Anchor adds validation, async handling, advanced state management patterns, and portability. Use runes for simple reactive state, use Anchor when you need more or when building reusable business logic that might be used in other frameworks.
Integration with Svelte
- Works with standard Svelte components
- Proper cleanup when components are destroyed
- No additional providers or setup required
- Full compatibility with Svelte's component lifecycle
- Integrates seamlessly with Svelte's
bind:directives
State Management
1. Mutable State
Use for: Local component state, direct mutations
<script>
import { mutable } from '@anchorlib/svelte';
// Objects
const user = mutable({ name: 'John', age: 30 });
user.age++; // Direct mutation triggers updates
// Arrays
const todos = mutable([]);
todos.push({ text: 'New', done: false }); // Works perfectly
// Primitives (use .value)
const count = mutable(0);
count.value++;
// With methods (encapsulation)
const cart = mutable({
items: [],
add(product) { this.items.push(product); },
get total() { return this.items.reduce((sum, i) => sum + i.price, 0); }
});
</script>Configuration:
<script>
import { z } from 'zod';
const state = mutable({ ... }, {
schema: z.object({ ... }), // Zod validation
recursive: true // true (default) | false | 'flat'
});
</script>Computed Properties (using getters):
<script>
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});
console.log(cart.total); // 20
cart.price = 20;
console.log(cart.total); // 40
</script>2. Immutable State
Use for: Shared/global state, controlled access
// store.ts
import { immutable, writable } from '@anchorlib/svelte';
// Public: Read-only
export const userState = immutable({ name: 'John', role: 'Admin' });
// Private: Full write access
export const userControl = writable(userState);
userControl.name = 'Jane'; // Works
// Restricted: Only specific keys
export const themeControl = writable(userState, ['theme']);
themeControl.theme = 'dark'; // Works
themeControl.name = 'X'; // Error!Best Practice: Always prefer immutable + writable for shared state to enforce clear contracts.
3. Derived State
Use for: Computed values across multiple reactive sources
<script>
import { mutable, derived } from '@anchorlib/svelte';
const count = mutable(1);
const count2 = mutable(5);
const counter = mutable({ count: 3 });
// Derived state that automatically updates when dependencies change
const total = derived(() => count.value + count2.value + counter.count);
console.log(total.value); // 9
count.value++;
console.log(total.value); // 10
</script>Intrinsic (within object) - Use getters:
<script>
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});
</script>Composite (across objects) - Use derived():
<script>
const todos = mutable([...]);
const filter = mutable('all');
const visibleTodos = derived(() => {
if (filter.value === 'completed') return todos.filter(t => t.done);
return todos;
});
</script>4. Form State with Validation
Use for: Forms with schema validation and automatic error tracking
<script>
import { form } from '@anchorlib/svelte';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
age: z.number().min(18),
});
const [state, errors] = form(schema, {
email: '',
password: '',
age: 0,
});
const submit = (e) => {
e.preventDefault();
const result = schema.safeParse(state);
if (result.success) {
api.signup(result.data);
}
};
</script>
<form onsubmit={submit}>
<input bind:value={state.email} />
{#if errors.email}
<span class="error">{errors.email.message}</span>
{/if}
<input type="password" bind:value={state.password} />
{#if errors.password}
<span class="error">{errors.password.message}</span>
{/if}
<button type="submit">Sign Up</button>
</form>Options:
<script>
const [state, errors] = form(schema, init, {
onChange: (event) => console.log('Changed:', event),
safeInit: false, // Show validation errors on initial render
});
</script>Component Architecture
Standard Svelte Components
Anchor works with standard Svelte components. No special wrappers needed.
<script>
import { mutable } from '@anchorlib/svelte';
const state = mutable({ count: 0 });
</script>
<div>
<h1>Count: {state.count}</h1>
<button onclick={() => state.count++}>Increment</button>
</div>Global State
State declared outside components is shared globally:
// store.ts
import { mutable } from '@anchorlib/svelte';
// Global state
export const counter = mutable({ count: 0 });<!-- Counter.svelte -->
<script>
import { counter } from './store';
</script>
<div>
<h1>Counter: {counter.count}</h1>
<button onclick={() => counter.count++}>Increment</button>
</div>⚠️ SSR Warning: Module-level state is shared across all requests in SSR environments. Use Svelte context for request-scoped state.
Headless State (Reusable Logic)
Separate business logic from UI using factory functions or classes:
Factory Function:
// stores/counter.ts
import { mutable } from '@anchorlib/svelte';
export function createCounter() {
return mutable({
count: 0,
increment() { this.count++; },
decrement() { this.count--; }
});
}<!-- Usage -->
<script>
import { createCounter } from './stores/counter';
const counter = createCounter();
</script>
<button onclick={() => counter.increment()}>{counter.count}</button>Class Pattern:
import { mutable } from '@anchorlib/svelte';
export class TabState {
public active = 'home';
setActive(tab: string) {
this.active = tab;
}
}
export function createTab(active?: string) {
return mutable(new TabState(active));
}Two-Way Data Binding
Using Svelte's Native Binding
Svelte's bind: directive works perfectly with Anchor state:
<script>
import { mutable } from '@anchorlib/svelte';
const user = mutable({
name: 'John',
email: 'john@example.com',
age: 30
});
</script>
<input bind:value={user.name} />
<input bind:value={user.email} />
<input type="number" bind:value={user.age} />
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>Binding to Nested Properties
<script>
const state = mutable({
user: {
profile: {
name: 'John'
}
}
});
</script>
<!-- Direct binding to nested properties -->
<input bind:value={state.user.profile.name} />Checkbox and Radio Bindings
<script>
const todo = mutable({ done: false, text: 'Task 1' });
const settings = mutable({ theme: 'dark' });
</script>
<!-- Checkbox -->
<input type="checkbox" bind:checked={todo.done} />
<!-- Radio -->
<input type="radio" bind:group={settings.theme} value="light" />
<input type="radio" bind:group={settings.theme} value="dark" />Async Handling
1. Query State
Use for: General async operations with full control
<script>
import { query } from '@anchorlib/svelte';
const user = query(async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
}, { name: 'Guest' }); // Initial data
</script>
{#if user.status === 'pending'}
<p>Loading...</p>
{:else if user.status === 'error'}
<p>Error: {user.error?.message}</p>
{:else if user.status === 'success'}
<p>Hello, {user.data.name}!</p>
{/if}Features:
- Auto-Status:
statusproperty ('idle' | 'pending' | 'success' | 'error') - Auto-Cancellation: Aborts previous request if a new one starts
- Manual Cancellation: Call
.abort()to cancel - Signal Passing: Passes
AbortSignalto async function
Deferred Execution:
<script>
const userQuery = query(
async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
},
undefined,
{ deferred: true }
);
// Later, when ready
userQuery.start();
</script>Re-fetching:
<script>
const dataQuery = query(fetchData, []);
// Refresh the data
function refresh() {
dataQuery.start(); // Cancels previous request automatically
}
</script>2. Fetch State
Use for: HTTP requests with automatic response handling
<script>
import { fetchState } from '@anchorlib/svelte';
const userState = fetchState(
{ name: '', email: '' },
{ url: '/api/user' }
);
// POST request
const createUser = fetchState(
null,
{
url: '/api/users',
method: 'POST',
body: { name: 'John', email: 'john@example.com' }
}
);
// Dynamic requests
const dataState = fetchState(
null,
{ url: '/api/data', deferred: true }
);
dataState.fetch({
url: '/api/data/123',
headers: { 'Authorization': 'Bearer token' }
});
</script>3. Stream State
Use for: Streaming responses that arrive in chunks
<script>
import { streamState } from '@anchorlib/svelte';
const chatStream = streamState(
'',
{ url: '/api/chat/stream' }
);
// Data updates incrementally as chunks arrive
console.log(chatStream.data);
</script>Transform Function:
<script>
const logStream = streamState(
[],
{
url: '/api/logs/stream',
transform: (current, chunk) => {
return [...current, chunk];
}
}
);
</script>Promise Integration
All async state functions expose a .promise property:
<script>
const userQuery = query(fetchUser);
await userQuery.promise;
console.log('User loaded:', userQuery.data);
</script>Reactivity System
Effects
Use for: Side effects that respond to Anchor state changes
Important: Anchor's effect() only tracks Anchor state (created with mutable(), immutable(), derived(), etc.). It cannot track Svelte stores. For Svelte stores, use $: reactive statements instead.
<script>
import { mutable, effect } from '@anchorlib/svelte';
import { writable } from 'svelte/store';
const anchorState = mutable({ count: 0 });
const svelteStore = writable(0);
// ✅ Works: Tracks Anchor state
effect(() => {
console.log('Anchor count:', anchorState.count);
});
// ❌ Won't work: Cannot track Svelte stores
effect(() => {
console.log('Store:', $svelteStore); // Will NOT re-run when store changes
});
// ✅ Use Svelte reactive statements for stores
$: console.log('Store:', $svelteStore); // Works correctly
</script>Basic Usage:
<script>
const state = mutable({ count: 0 });
effect(() => {
console.log('Count:', state.count); // Tracks state.count
// Re-runs when state.count changes
});
// With cleanup
effect(() => {
const id = setInterval(() => console.log('Tick'), state.delay);
return () => clearInterval(id); // Cleanup on re-run or unmount
});
</script>Dynamic Tracking:
<script>
effect(() => {
if (state.showDetails) {
console.log(state.details); // Only tracks when showDetails is true
}
});
</script>Untracking:
<script>
import { untrack } from '@anchorlib/svelte';
effect(() => {
const content = doc.content; // Tracked
const endpoint = untrack(() => settings.saveUrl); // Not tracked
untrack(() => {
fetch(endpoint, { body: content }); // No tracking inside
});
});
</script>Snapshots:
<script>
import { snapshot } from '@anchorlib/svelte';
effect(() => {
if (state.query) {
const copy = snapshot(state); // Deep clone, not reactive
const json = JSON.stringify(copy); // Safe
}
});
</script>Subscribe (Global Observability)
Use for: Listening to any change in an object
<script>
import { subscribe } from '@anchorlib/svelte';
subscribe(user, (val, event) => {
console.log('Changed:', event);
}, true); // recursive = true (default)
</script>Lifecycle
onMount
Use for: DOM access, 3rd-party libs, animations
<script>
import { onMount } from 'svelte';
let inputRef;
onMount(() => {
inputRef?.focus();
});
</script>
<input bind:this={inputRef} />onDestroy
Use for: Cleanup when component unmounts
<script>
import { onDestroy } from 'svelte';
const handleResize = () => console.log('Resized');
window.addEventListener('resize', handleResize);
onDestroy(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<div>Component</div>Advanced Patterns
State Persistence
Use for: Syncing state with localStorage or sessionStorage
<script>
import { persistent, session } from '@anchorlib/svelte/storage';
// Persists to localStorage (survives restart)
const settings = persistent('app-settings', {
theme: 'dark'
});
// Persists to sessionStorage (tab session only)
const draft = session('form-draft', {
title: ''
});
</script>State Serialization
Use for: Safely serializing reactive state
<script>
import { stringify } from '@anchorlib/core';
const state = mutable({
user: { name: 'John', age: 30 },
todos: [{ text: 'Task 1', done: false }],
});
// ❌ Don't use JSON.stringify() - subscribes to all properties
// const json = JSON.stringify(state);
// ✅ Use stringify() - reads without subscribing
const json = stringify(state);
// Save to localStorage safely
localStorage.setItem('app-state', stringify(state));
</script>Optimistic UI
Use for: UI updates that can be undone
<script>
import { undoable } from '@anchorlib/svelte';
const handleClick = () => {
const [rollback, settled] = undoable(() => {
props.liked = !props.liked;
});
updateLike(props.liked).then(settled).catch(rollback);
};
</script>
<button onclick={handleClick}>
{props.liked ? 'Unlike' : 'Like'}
</button>Svelte Integration
Control Flow
Use Svelte's built-in control flow for conditional rendering and lists:
Conditional rendering:
{#if user.isLoggedIn}
<Dashboard {user} />
{:else}
<Login />
{/if}List rendering:
<script>
const todos = mutable([
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: true },
]);
</script>
<ul>
{#each todos as todo}
<li>
<input
type="checkbox"
bind:checked={todo.done}
/>
{todo.text}
</li>
{/each}
</ul>Multiple conditions:
{#if status === 'loading'}
<Spinner />
{:else if status === 'error'}
<Error />
{:else if status === 'success'}
<Content />
{/if}Await blocks:
{#await user.promise}
<Loading />
{:then data}
<p>Hello, {data.name}!</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}TypeScript
<script lang="ts">
import type { Bindable, BindableProp } from '@anchorlib/svelte';
// Component with props
interface Props {
value: number;
onChange?: (val: number) => void;
}
const { value, onChange }: Props = $props();
// Mutable with type
const state = mutable<{ count: number }>({ count: 0 });
// Query with type
interface User {
name: string;
email: string;
}
const userQuery = query<User>(async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
}, { name: '', email: '' });
</script>Best Practices
1. Encapsulate Logic with Data
Group related data and behavior together in objects:
<script>
const todoApp = mutable({
newText: '',
todos: [],
addTodo() {
this.todos.push({ text: this.newText });
this.newText = '';
}
});
</script>2. Direct Mutations
Direct property updates:
<script>
// ✅ Logic-Driven
user.age++;
todos.push(newItem);
</script>3. Automatic Dependency Tracking
Effects automatically track dependencies - no manual arrays needed:
<script>
effect(() => {
fetchUser(state.userId).then(d => state.data = d);
}); // Automatically tracks state.userId
</script>4. Use bind: for Forms
<script>
const state = mutable({
email: '',
password: '',
});
</script>
<form>
<input bind:value={state.email} />
<input type="password" bind:value={state.password} />
<button type="submit">Sign Up</button>
</form>5. Computed Properties with Getters
Use JavaScript getters for derived values within objects:
<script>
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});
</script>6. Prefer Immutable for Shared State
Use immutable() + writable() for controlled access to shared state:
// Public: Read-only
export const state = immutable({ count: 0 });
// Private: Write contract
export const stateControl = writable(state);Common Pitfalls
❌ Using Module-Level State in SSR
// ⚠️ SSR Risk: Shared across all requests!
export const appState = mutable({ theme: 'dark' });Fix: Use Svelte context for request-scoped state in SSR.
❌ Not Using Initial Data for Queries
<script>
// ❌ Requires defensive checks
const user = query(fetchUser);
console.log(user.data?.name); // Need optional chaining
// ✅ Safe to access immediately
const user = query(fetchUser, { name: '', email: '' });
console.log(user.data.name); // Always works
</script>❌ Using effect() with Svelte Stores
<script>
import { effect } from '@anchorlib/svelte';
import { writable } from 'svelte/store';
const count = writable(0);
// ❌ Won't work: effect() cannot track Svelte stores
effect(() => {
console.log($count); // Will NOT re-run when store changes
});
// ✅ Use Svelte reactive statements for stores
$: console.log($count); // Works correctly
// ✅ Use Anchor effect only for Anchor state
const anchorState = mutable({ count: 0 });
effect(() => {
console.log(anchorState.count); // Works correctly
});
</script>❌ Using JSON.stringify() on Reactive State
<script>
// ❌ Subscribes to all properties
const json = JSON.stringify(state);
// ✅ Use stringify() instead
import { stringify } from '@anchorlib/core';
const json = stringify(state);
</script>Quick Reference
Imports
<script>
import {
// State
mutable, immutable, writable, derived,
// Async
query, fetchState, streamState,
// Form
form,
// Reactivity
effect, untrack, snapshot, subscribe,
// Utils
undoable
} from '@anchorlib/svelte';
// Storage (separate)
import { persistent, session } from '@anchorlib/svelte/storage';
// Utils (separate)
import { stringify } from '@anchorlib/core';
// Types
import type { Bindable, BindableProp } from '@anchorlib/svelte';
// Svelte
import { onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
</script>Decision Trees
State Type:
- Local component state →
mutable() - Shared state →
immutable()+writable() - Computed value (same object) → getter
- Computed value (cross-object) →
derived() - Form with validation →
form()
Async Operations:
- General async →
query() - HTTP requests →
fetchState() - Streaming →
streamState()
When to Use:
effect()→ Side effects for Anchor state only (use$:for Svelte stores)onMount→ DOM access after mount (Svelte)onDestroy→ Cleanup on unmount (Svelte)bind:value→ Two-way bindinguntrack→ Read without subscribingpersistent()→ localStorage syncsession()→ sessionStorage sync
Performance Optimization
- Use
derived()for cross-object computations - Auto-cached - Use getters for derived state within objects - Auto-cached
- Use
untrackfor expensive reads - Avoid over-subscription - Use Svelte control flow -
{#if},{#each},{#await} - Provide initial data for queries - Avoid undefined states
- Use
bind:for forms - Native Svelte two-way binding
Key Principles for AI
- Use standard Svelte components - No special wrappers needed
- Use
bind:for two-way binding - Svelte's native binding works with Anchor - Use Svelte control flow -
{#if},{#each},{#await} - Use
mutable()for local state - Direct mutation works - Use
immutable()+writable()for shared state - Enforce contracts - Use getters for computed properties - Within same object
- Use
derived()for cross-object computations - Across multiple sources - Use
query()for async operations - Built-in status tracking - Use
form()for forms with validation - Automatic error tracking - Provide initial data for queries - Avoid undefined states
- Use
effect()for Anchor state side effects - Only tracks Anchor state, not Svelte stores - Use
$:for Svelte stores - Anchor'seffect()cannot track stores
Success Checklist
- [ ] Using standard Svelte components
- [ ] State uses
mutable()orimmutable() - [ ] Two-way binding uses
bind:valueorbind:checked - [ ] Conditional rendering uses
{#if} - [ ] List rendering uses
{#each} - [ ] Async operations use
query(),fetchState(), orstreamState() - [ ] Forms use
form()with Zod schema - [ ] Effects use
effect()for automatic tracking - [ ] Shared state uses
immutable()+writable() - [ ] Computed properties use getters or
derived()