Anchor for SolidJS - AI Knowledge Base
Reference for AI assistants to build robust SolidJS applications using Anchor.
Core Concepts
What is Anchor for SolidJS?
Anchor enhances SolidJS's reactivity system by providing:
- Direct Mutation: Mutate state directly while maintaining reactivity
- True Immutability: Controlled mutations with read/write contracts
- Schema Validation: Runtime validation with Zod
- Data Integrity: Ensure state maintains structure and types
Integration with SolidJS
- Automatic tracking binding with Solid's reactivity system
- Proper cleanup when components are destroyed
- No additional providers or setup required
- Full compatibility with Solid's component lifecycle
State Management
1. Mutable State
Use for: Local component state, direct mutations
import { mutable } from '@anchorlib/solid';
// 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); }
});Configuration:
const state = mutable({ ... }, {
schema: z.object({ ... }), // Zod validation
recursive: true // true (default) | false | 'flat'
});Computed Properties (using getters):
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); // 402. Immutable State
Use for: Shared/global state, controlled access
import { immutable, writable } from '@anchorlib/solid';
// 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
import { mutable, derived } from '@anchorlib/solid';
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); // 10Intrinsic (within object) - Use getters:
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});Composite (across objects) - Use derived():
const todos = mutable([...]);
const filter = mutable('all');
const visibleTodos = derived(() => {
if (filter.value === 'completed') return todos.filter(t => t.done);
return todos;
});4. Form State with Validation
Use for: Forms with schema validation and automatic error tracking
import { form } from '@anchorlib/solid';
import { z } from 'zod';
import { Show } from 'solid-js';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
age: z.number().min(18),
});
export const SignUpForm = () => {
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);
}
};
return (
<form onSubmit={submit}>
<input
value={state.email}
onInput={(e) => state.email = e.currentTarget.value}
/>
<Show when={errors.email}>
<span class="error">{errors.email.message}</span>
</Show>
<input
type="password"
value={state.password}
onInput={(e) => state.password = e.currentTarget.value}
/>
<Show when={errors.password}>
<span class="error">{errors.password.message}</span>
</Show>
<button type="submit">Sign Up</button>
</form>
);
};Options:
form(schema, init, {
onChange: (event) => console.log('Changed:', event),
safeInit: false, // Show validation errors on initial render
});Component Architecture
Standard SolidJS Components
Anchor works with standard SolidJS function components. No special wrappers needed for basic usage.
import { mutable } from '@anchorlib/solid';
const Counter = () => {
const state = mutable({ count: 0 });
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={() => state.count++}>Increment</button>
</div>
);
};Global State
State declared outside components is shared globally:
import { mutable } from '@anchorlib/solid';
// Global state
const counter = mutable({ count: 0 });
const Counter = () => {
return (
<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 Solid 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/solid';
export function createCounter() {
return mutable({
count: 0,
increment() { this.count++; },
decrement() { this.count--; }
});
}
// Usage
const Counter = () => {
const counter = createCounter();
return <button onClick={() => counter.increment()}>{counter.count}</button>;
};Class Pattern:
import { mutable } from '@anchorlib/solid';
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
Creating Bindable Components
Use the bindable() HOC to create components that accept bindable props:
import { bindable, $bind } from '@anchorlib/solid';
import type { Bindable } from '@anchorlib/solid';
// Bindable input component
interface InputProps {
value: Bindable<string>;
label?: string;
}
const Input = bindable<InputProps>((props) => {
return (
<div>
{props.label && <label>{props.label}</label>}
<input
type="text"
value={props.value}
onInput={(e) => (props.value = e.currentTarget.value)}
/>
</div>
);
});
// Usage with $bind()
const App = () => {
const user = mutable({
firstName: 'John',
lastName: 'Doe',
});
return (
<div>
<Input label="First Name" value={$bind(user, 'firstName')} />
<Input label="Last Name" value={$bind(user, 'lastName')} />
<p>Full Name: {user.firstName} {user.lastName}</p>
</div>
);
};Binding to MutableRef
const count = mutable(0);
// Bind to MutableRef - no key needed
<Counter value={$bind(count)} />Binding to Object Properties
const state = mutable({ name: 'John', age: 30 });
// Bind to specific properties
<TextInput value={$bind(state, 'name')} />
<NumberInput value={$bind(state, 'age')} />Type Safety
Important: To use $bind(), the component prop must be typed with Bindable<T>:
interface TextInputProps {
value: Bindable<string>; // ← Required for $bind()
}
const TextInput = bindable<TextInputProps>((props) => {
return <input value={props.value} onInput={...} />;
});
// ✅ Correct: Using $bind() with Bindable<string> prop
const state = mutable({ name: 'John' });
<TextInput value={$bind(state, 'name')} />
// ❌ Type Error: Cannot use $bind() with regular string prop
interface BadInputProps {
value: string; // Not Bindable<string>
}Optional Bindable Props
Use BindableProp<T> for props that can accept both regular values and bindings:
import type { BindableProp } from '@anchorlib/solid';
interface InputProps {
value: BindableProp<string>; // Can be string OR Bindable<string>
}
const Input = bindable<InputProps>((props) => {
return <input value={props.value} onInput={...} />;
});
// Can be used with or without binding
<Input value={$bind(state, 'name')} />
<Input value="Static value" />Props Filtering
Bindable components have access to $omit() and $pick() utility methods:
interface MyComponentProps {
value: Bindable<string>;
label?: string;
placeholder?: string;
disabled?: boolean;
class?: string;
}
const MyComponent = bindable<MyComponentProps>((props) => {
// Omit specific props before spreading
const inputProps = props.$omit(['label', 'class']);
return (
<div class={props.class}>
{props.label && <label>{props.label}</label>}
<input {...inputProps} />
</div>
);
});Async Handling
1. Query State
Use for: General async operations with full control
import { query } from '@anchorlib/solid';
import { Switch, Match } from 'solid-js';
const UserProfile = () => {
const user = query(async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
}, { name: 'Guest' }); // Initial data
return (
<Switch>
<Match when={user.status === 'pending'}>
<p>Loading...</p>
</Match>
<Match when={user.status === 'error'}>
<p>Error: {user.error?.message}</p>
</Match>
<Match when={user.status === 'success'}>
<p>Hello, {user.data.name}!</p>
</Match>
</Switch>
);
};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:
const userQuery = query(
async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
},
undefined,
{ deferred: true }
);
// Later, when ready
userQuery.start();Re-fetching:
const dataQuery = query(fetchData, []);
// Refresh the data
function refresh() {
dataQuery.start(); // Cancels previous request automatically
}2. Fetch State
Use for: HTTP requests with automatic response handling
import { fetchState } from '@anchorlib/solid';
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' }
});3. Stream State
Use for: Streaming responses that arrive in chunks
import { streamState } from '@anchorlib/solid';
const chatStream = streamState(
'',
{ url: '/api/chat/stream' }
);
// Data updates incrementally as chunks arrive
console.log(chatStream.data);Transform Function:
const logStream = streamState(
[],
{
url: '/api/logs/stream',
transform: (current, chunk) => {
return [...current, chunk];
}
}
);Promise Integration
All async state functions expose a .promise property:
const userQuery = query(fetchUser);
await userQuery.promise;
console.log('User loaded:', userQuery.data);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 SolidJS signals created with createSignal(). For SolidJS signals, use SolidJS's createEffect() instead.
import { effect } from '@anchorlib/solid';
import { createSignal, createEffect } from 'solid-js';
const anchorState = mutable({ count: 0 });
const [solidSignal, setSolidSignal] = createSignal(0);
// ✅ Works: Tracks Anchor state
effect(() => {
console.log('Anchor count:', anchorState.count);
});
// ❌ Won't work: Cannot track SolidJS signals
effect(() => {
console.log('Signal:', solidSignal()); // Will NOT re-run when signal changes
});
// ✅ Use SolidJS createEffect for signals
createEffect(() => {
console.log('Signal:', solidSignal()); // Works correctly
});Basic Usage:
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
});Dynamic Tracking:
effect(() => {
if (state.showDetails) {
console.log(state.details); // Only tracks when showDetails is true
}
});Untracking:
import { untrack } from '@anchorlib/solid';
effect(() => {
const content = doc.content; // Tracked
const endpoint = untrack(() => settings.saveUrl); // Not tracked
untrack(() => {
fetch(endpoint, { body: content }); // No tracking inside
});
});Snapshots:
import { snapshot } from '@anchorlib/solid';
effect(() => {
if (state.query) {
const copy = snapshot(state); // Deep clone, not reactive
const json = JSON.stringify(copy); // Safe
}
});Subscribe (Global Observability)
Use for: Listening to any change in an object
import { subscribe } from '@anchorlib/solid';
subscribe(user, (val, event) => {
console.log('Changed:', event);
}, true); // recursive = true (default)Lifecycle
onMount
Use for: DOM access, 3rd-party libs, animations
import { onMount } from 'solid-js';
const Component = () => {
let inputRef;
onMount(() => {
inputRef?.focus();
});
return <input ref={inputRef} />;
};onCleanup
Use for: Cleanup when component unmounts
import { onCleanup } from 'solid-js';
const Component = () => {
const handleResize = () => console.log('Resized');
window.addEventListener('resize', handleResize);
onCleanup(() => {
window.removeEventListener('resize', handleResize);
});
return <div>Component</div>;
};Advanced Patterns
State Persistence
Use for: Syncing state with localStorage or sessionStorage
import { persistent, session } from '@anchorlib/solid/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: ''
});State Serialization
Use for: Safely serializing reactive state
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));Optimistic UI
Use for: UI updates that can be undone
import { undoable } from '@anchorlib/solid';
const LikeButton = (props) => {
const handleClick = () => {
const [rollback, settled] = undoable(() => {
props.liked = !props.liked;
});
updateLike(props.liked).then(settled).catch(rollback);
};
return (
<button onClick={handleClick}>
{props.liked ? 'Unlike' : 'Like'}
</button>
);
};SolidJS Integration
Control Flow Components
Use SolidJS's built-in control flow components for conditional rendering and lists:
Show - Conditional rendering:
import { Show } from 'solid-js';
<Show when={user.isLoggedIn} fallback={<Login />}>
<Dashboard user={user} />
</Show>For - List rendering:
import { For } from 'solid-js';
const TodoList = () => {
const todos = mutable([
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: true },
]);
return (
<ul>
<For each={todos}>
{(todo) => (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => todo.done = !todo.done}
/>
{todo.text}
</li>
)}
</For>
</ul>
);
};Switch/Match - Multiple conditions:
import { Switch, Match } from 'solid-js';
<Switch>
<Match when={status === 'loading'}>
<Spinner />
</Match>
<Match when={status === 'error'}>
<Error />
</Match>
<Match when={status === 'success'}>
<Content />
</Match>
</Switch>TypeScript
// Component with props
interface Props {
value: number;
onChange?: (val: number) => void;
}
const Counter = (props: Props) => {
return <div>{props.value}</div>;
};
// Component with two-way binding
interface TextInputProps {
value: Bindable<string>; // Two-way binding
placeholder?: string;
}
const TextInput = bindable<TextInputProps>((props) => {
return (
<input
value={props.value}
placeholder={props.placeholder}
onInput={(e) => props.value = e.currentTarget.value}
/>
);
});
// 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: '' });Best Practices
1. Encapsulate Logic with Data
Group related data and behavior together in objects:
const todoApp = mutable({
newText: '',
todos: [],
addTodo() {
this.todos.push({ text: this.newText });
this.newText = '';
}
});2. Direct Mutations
Direct property updates:
// ✅ Logic-Driven
user.age++;3. Automatic Dependency Tracking
Effects automatically track dependencies - no manual arrays needed:
effect(() => {
fetchUser(state.userId).then(d => state.data = d);
}); // Automatically tracks state.userId4. Use Bindable Components for Forms
// ✅ Reusable bindable component
const SignUp = () => {
const state = mutable({
email: '',
password: '',
});
return (
<form>
<TextInput value={$bind(state, 'email')} />
<TextInput type="password" value={$bind(state, 'password')} />
<button type="submit">Sign Up</button>
</form>
);
};5. Computed Properties with Getters
Use JavaScript getters for derived values within objects:
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});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 Solid Context for request-scoped state in SSR.
❌ Not Using Initial Data for Queries
// ❌ 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❌ Using effect() with SolidJS Signals
import { effect } from '@anchorlib/solid';
import { createSignal, createEffect } from 'solid-js';
const [count, setCount] = createSignal(0);
// ❌ Won't work: effect() cannot track SolidJS signals
effect(() => {
console.log(count()); // Will NOT re-run when signal changes
});
// ✅ Use SolidJS createEffect for signals
createEffect(() => {
console.log(count()); // Works correctly
});
// ✅ Use Anchor effect only for Anchor state
const anchorState = mutable({ count: 0 });
effect(() => {
console.log(anchorState.count); // Works correctly
});Quick Reference
Imports
import {
// State
mutable, immutable, writable, derived,
// Binding
$bind, bindable,
// Async
query, fetchState, streamState,
// Form
form,
// Reactivity
effect, untrack, snapshot, subscribe,
// Storage
persistent, session,
// Utils
stringify, undoable
} from '@anchorlib/solid';
// Types
import type { Bindable, BindableProp } from '@anchorlib/solid';
// SolidJS
import { Show, For, Switch, Match, onMount, onCleanup } from 'solid-js';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 (usecreateEffect()for SolidJS signals)onMount→ DOM access after mount (SolidJS)onCleanup→ Cleanup on unmount (SolidJS)$bind()→ Two-way bindingbindable()→ Make component accept bindable propsuntrack→ 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 SolidJS control flow -
<Show>,<For>,<Switch>,<Match> - Provide initial data for queries - Avoid undefined states
- Use
bindable()for reusable form components - Reduce boilerplate
Key Principles for AI
- Use standard SolidJS function components - No special wrappers needed
- Use
bindable()HOC for components that accept bindable props - Use
$bind()for two-way binding - Prop must be typed withBindable<T> - Use SolidJS control flow -
<Show>,<For>,<Switch>,<Match> - 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 SolidJS signals - Use
createEffect()for SolidJS signals - Anchor'seffect()cannot track signals
Success Checklist
- [ ] Using standard SolidJS function components
- [ ] State uses
mutable()orimmutable() - [ ] Bindable components use
bindable()HOC - [ ] Two-way binding uses
$bind()withBindable<T>props - [ ] Conditional rendering uses
<Show>,<Switch>,<Match> - [ ] List rendering uses
<For> - [ ] Async operations use
query(),fetchState(), orstreamState() - [ ] Forms with validation use
form() - [ ] Effects use
effect()for automatic tracking - [ ] Shared state uses
immutable()+writable()