Anchor for React - AI Knowledge Base
Reference for AI assistants to build robust React applications using Anchor.
Terminology
Before diving into Anchor, understand these core terms:
Component
The Logic Layer of your application. Created with setup().
- Runs: Once on mount
- Contains: State, logic, effects, lifecycle handlers
- Nature: Stateful - manages reactive state and behavior
- Purpose: Define what your component does
export const Counter = setup(() => {
// Component: Logic Layer (runs once)
const state = mutable({ count: 0 });
const increment = () => state.count++;
// Return a View
return render(() => <button onClick={increment}>{state.count}</button>);
});View
The Presentation Layer of your application. Displays UI and responds to state changes.
- Runs: Reactively when dependencies change
- Contains: JSX that reads state
- Nature: Stateless - no internal state, only displays data
- Purpose: Define what your component looks like
Three types of Views:
Component View - Created with
render(), the primary output tied to the Componenttsxreturn render(() => <div>{state.value}</div>);Snippet - Created with
snippet(), stateless, local, has scope accesstsxconst Counter = snippet(() => <span>{count.value}</span>);Template - Created with
template(), stateless, reusable, no scope accesstsxconst UserCard = template<{ user: User }>(({ user }) => ( <div>{user.name}</div> ));
Key distinction:
- Component = Stateful (has state)
- Snippet = Stateless, local (accesses component scope)
- Template = Stateless, reusable (props only)
Props
Props in Anchor are reactive proxies, unlike React's plain objects. This enables them to be writable and support pass-by-reference.
// Anchor props are reactive proxies
export const Card = setup<CardProps>((props) => {
return render(() => (
<div className={`card-${props.variant}`}>
{props.children}
</div>
));
});Key difference from React:
- React: Props are plain objects, passed by value, read-only
- Anchor: Props are reactive proxies, can be writable, enabling pass-by-reference with
$bind()and$use()
// React: Props are read-only
function ReactComponent(props) {
props.value = 'new'; // ❌ Error or ignored
}
// Anchor: Props can be writable (if declared with Bindable<T>)
export const AnchorComponent = setup<{ value: Bindable<string> }>((props) => {
props.value = 'new'; // ✅ Works - writes back to parent
});Binding
Pass-by-reference for reactive state between Anchor components.
$bind(): Two-way binding (child can read and write)$use(): One-way binding (child can only read)- Important: Only works with Anchor components, not standard HTML elements
// Parent
const state = mutable({ email: '' });
// Two-way binding
<TextInput value={$bind(state, 'email')} />
// One-way binding
<Display text={$use(state, 'email')} />
// Child with two-way binding must declare Bindable<T> type
export type TextInputProps = {
value: Bindable<string>; // Two-way binding
};
export const TextInput = setup<TextInputProps>((props) => {
// ✅ TypeScript allows write because value is Bindable<string>
const handleChange = (e) => props.value = e.target.value;
return render(() => <input value={props.value} onChange={handleChange} />);
});
// Child with one-way binding uses regular type
export type DisplayProps = {
text: string; // One-way, just reads the value
};
export const Display = setup<DisplayProps>((props) => {
// ❌ TypeScript error: Cannot assign to 'text' because it is a read-only property
// props.text = 'new value';
return render(() => <span>{props.text}</span>);
});TypeScript enforcement: Props are strongly typed. If you attempt to assign to props not declared with Bindable<T>, TypeScript will warn you.
Core Architecture
Fundamental Shift
Anchor solves React's core problem: The Rendering Model. It separates components into two layers:
- Component (Logic Layer) - Runs once when created. Contains state, logic, and effects. Never re-executes.
- View (Presentation Layer) - Runs reactively. Updates only when specific dependencies change.
Key Benefit: Eliminates re-render cascades, stale closures, and dependency arrays.
import { setup, mutable, render } from '@anchorlib/react';
export const Counter = setup(() => {
// Logic Layer - runs once
const state = mutable({ count: 0 });
const increment = () => state.count++;
// Presentation Layer - reactive
return render(() => (
<button onClick={increment}>{state.count}</button>
));
}, 'Counter');Pass-By-Reference for Reactive State
Core principle: Always use $bind() or $use() when passing reactive state to Anchor components.
Why? Pass-by-value requires parent re-renders. Pass-by-reference enables child components to update reactively without parent re-renders.
Important: Pass-by-reference only works with Anchor components (components created with setup()). It does not work with standard HTML elements or 3rd-party components not built with Anchor.
// ❌ Pass-by-value: Parent must re-render to update child
<Counter value={state.count} />
// ✅ Pass-by-reference: Child updates reactively without parent re-render
<Counter value={$use(state, 'count')} />View Scope Decision Tree
When to use render():
- Simple components where full re-render won't hurt performance
- Component has minimal reactive state
- No need for fine-grained updates
export const SimpleCard = setup(() => {
const state = mutable({ title: 'Card', count: 0 });
// Simple component - just use render()
return render(() => (
<div>
<h2>{state.title}</h2>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
));
});When NOT to use render():
- ❌ Don't wrap everything in
render()by default - it makes the entire component reactive - ✅ Use static JSX for structural elements that never change
- ✅ Use snippets to isolate reactive parts from static layout
When to use snippet():
- Related UI that updates together - Group related reactive values in a single snippet
- Offload reactive UI from static component - Most of the UI is static, only small parts are reactive
Example 1: Related UI updates together
export const TodoList = setup(() => {
const todos = mutable([
{ text: 'Task 1', done: false },
{ text: 'Task 2', done: true },
]);
const stats = derived(() => ({
total: todos.length,
active: todos.filter(t => !t.done).length,
completed: todos.filter(t => t.done).length,
}));
const Stats = snippet(() => (
<div>
<span>Total: {stats.value.total}</span>
<span>Active: {stats.value.active}</span>
<span>Done: {stats.value.completed}</span>
</div>
));
return (
<div>
<Stats />
{/* ... todo list ... */}
</div>
);
});Example 2: Offload reactive UI from static component
export const Dashboard = setup(() => {
const count = mutable(0);
const Counter = snippet(() => <span>{count.value}</span>);
return (
<div className="dashboard"> {/* Static */}
<h1>Dashboard</h1> {/* Static */}
<p>Count: <Counter /></p> {/* Reactive snippet */}
<button onClick={() => count.value++}>+</button> {/* Static */}
</div>
);
});When snippets add value:
- ✅ Isolating reactive parts from static layout (like Counter in Dashboard above)
- ✅ Grouping related reactive values (like Stats showing total/active/completed)
- ✅ Reusing view logic within the same component
- ❌ Don't create snippets for every single
<span>or<div>- use judgment based on reactivity needs
When to use template():
- Stateless, reusable views with no component scope access
- Can be used across different components
- Good for list items, cards, buttons, or any reusable UI element
// Reusable card template
const UserCard = template<{ user: User }>(({ user }) => (
<div className="card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
));
// Use in different components
<UserCard user={currentUser} />
<UserCard user={selectedUser} />When to create reusable components:
- Common patterns like inputs with two-way binding
- Better than creating new snippet for each instance
- Example:
<TextInput>component withBindable<string>prop
// ❌ Verbose: New snippet for each input
const EmailInput = snippet(() => (
<input value={state.email} onChange={e => state.email = e.target.value} />
));
const PasswordInput = snippet(() => (
<input type="password" value={state.password} onChange={e => state.password = e.target.value} />
));
// ✅ Better: Reusable component
<TextInput value={$bind(state, 'email')} />
<TextInput type="password" value={$bind(state, 'password')} />State Management
1. Mutable State
- Use for: Local component state, direct mutations
- Pattern: Wrap objects/arrays in
mutable()to create reactive proxies
// 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'
});2. Immutable State
- Use for: Shared/global state, controlled access
- Pattern: Read-only public interface + writable contracts
// 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 that auto-update
Intrinsic (within object):
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});Composite (across objects):
const todos = mutable([...]);
const filter = mutable('all');
const visibleTodos = derived(() => {
if (filter.value === 'completed') return todos.filter(t => t.done);
return todos;
});
console.log(visibleTodos.value);4. Form State with Validation
- Use for: Forms with schema validation and automatic error tracking
- Pattern: Use
form()to create state + errors tuple - Returns:
[state, errors]where errors update automatically on validation failures
import { form } from '@anchorlib/core';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18),
});
export const SignUpForm = setup(() => {
const [state, errors] = form(schema, {
email: '',
password: '',
age: 0,
});
const submit = () => {
if (Object.keys(errors).length === 0) {
api.signup(state);
}
};
// Reactive error display
const ErrorMessage = snippet<{ field: string }>(({ field }) => {
const error = errors[field];
return error ? <span className="error">{error.message}</span> : null;
});
// Static layout with reactive error snippets
return (
<form onSubmit={submit}>
<TextInput value={$bind(state, 'email')} />
<ErrorMessage field="email" />
<TextInput type="password" value={$bind(state, 'password')} />
<ErrorMessage field="password" />
<NumberInput value={$bind(state, 'age')} />
<ErrorMessage field="age" />
<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
});6. State Serialization
- Use for: Safely serializing reactive state without subscribing to all properties
- Pattern: Use
stringify()instead ofJSON.stringify()on reactive state - Why:
JSON.stringify()would subscribe to all properties when reading them, causing unnecessary reactivity tracking.stringify()reads the state without subscribing.
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);
console.log(json); // {"user":{"name":"John","age":30},"todos":[{"text":"Task 1","done":false}]}
// With formatting
const formatted = stringify(state, null, 2);
// Save to localStorage safely
localStorage.setItem('app-state', stringify(state));7. State Scope Patterns
Local State (Component-scoped):
export const Counter = setup(() => {
const state = mutable({ count: 0 }); // Dies with component
// ...
});Headless State (Reusable logic):
// stores/counter.ts
export function createCounter() {
return mutable({
count: 0,
increment() { this.count++; }
});
}
// Use anywhere
const counter = createCounter();Global State (Module-scoped):
// ⚠️ SSR Risk: Shared across all requests!
export const appState = mutable({ theme: 'dark' });Component Architecture
1. Component (Logic Layer)
- Created with:
setup() - Runs: Once on mount
- Contains: State, logic, effects, lifecycle handlers
export const UserCard = setup((props) => {
// State
const state = mutable({ expanded: false });
// Logic
const toggle = () => state.expanded = !state.expanded;
// Effects
effect(() => console.log('Expanded:', state.expanded));
// Lifecycle
onMount(() => console.log('Mounted'));
onCleanup(() => console.log('Unmounted'));
// Return View
return render(() => <div onClick={toggle}>...</div>);
}, 'UserCard');Reactive Props:
- Props are reactive proxies that update in-place
- ⚠️ Never destructure props in setup body - breaks reactivity
- Use props directly in reactive boundaries (views, effects)
// ❌ Wrong
const { name } = props; // Captures initial value only
// ✅ Correct
effect(() => console.log(props.name)); // Tracks changes2. View (Presentation Layer)
Three types of Views:
A. Component View
- Use for: Primary component output
- Created with:
render() - Props: Can accept props (same as setup), allows destructuring
// Without props
return render(() => <div>{state.value}</div>);
// With props - can destructure since render() is a reactive boundary
export const Card = setup<CardProps>((props) => {
return render<typeof props>(({ variant, children }) => (
<div className={`card-${variant}`}>
{children}
</div>
));
});B. Template
- Use for: Stateless, reusable, props-driven views
- Created with:
template() - Scope: No access to component state
- Nature: Stateless - no internal state, only props
const UserCard = template<{ user: User }>(({ user }) => (
<div>{user.name}</div>
), 'UserCard');
// Pros: Stateless, reusable across components, testable
// Cons: No scope access, requires all data via propsC. Snippet
- Use for: Component-specific Views with scope access
- Created with:
snippet() - Scope: Full closure access to component state
export const Dashboard = setup(() => {
const state = mutable({ title: 'Dashboard' });
const Header = snippet(() => <h1>{state.title}</h1>, 'Header');
const Content = snippet(() => <main>...</main>, 'Content');
return (
<div>
<Header /> {/* Updates when state.title changes */}
<Content />
</div>
);
});
// Pros: Easy access, convenient, co-located
// Cons: Lower performance, not reusableSnippet Props:
const Item = snippet<{ text: string }>(
(props, parentProps) => ( // props + parentProps
<div className={`item theme-${parentProps.theme}`}>
{props.text}
</div>
),
'Item'
);D. Static Layout
- Use for: Structural JSX that never changes
- Pattern: Return JSX directly (no
render())
return (
<div className="layout"> {/* Static */}
<Header /> {/* Reactive */}
<div className="sidebar">Static</div>
<Content /> {/* Reactive */}
</div>
);3. Props Handling
Props are reactive proxies in all contexts. The difference is how you can use them based on whether you're in a reactive boundary.
In Component context (setup() body):
- Not in a reactive boundary (setup runs once)
- ⚠️ Never destructure - captures initial values only
- ⚠️ Never use
...restspread - logs error - ✅ Use
$omit()and$pick()for rest props
export const Card = setup<CardProps>((props) => {
const divProps = props.$omit(['variant']); // Exclude specific keys
return render(() => (
<div className={`card-${props.variant}`} {...divProps}>
{props.children}
</div>
));
});In View context (render(), template(), snippet() body):
- In a reactive boundary (Views re-run when dependencies change)
- ✅ Can destructure for reading - safe because Views re-run
- ✅ Can use
...restspread for reading - ⚠️ To write to props, access directly (no destructure)
// render() can accept and destructure props
export const Card = setup<CardProps>((props) => {
return render<typeof props>(({ variant, children }) => (
<div className={`card-${variant}`}>
{children}
</div>
));
});
// template() can destructure props
const Button = template<ButtonProps>(({ variant, ...rest }) => (
<button className={`btn-${variant}`} {...rest} />
));4. List Rendering
Template (self-contained items):
const TodoItem = template<{ todo: Todo }>(({ todo }) => (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => todo.done = !todo.done}
/>
{todo.text}
</li>
), 'TodoItem');Snippet (needs component functions):
export const TodoList = setup(() => {
const state = mutable({
todos: [],
remove(todo) {
const index = this.todos.indexOf(todo);
if (index !== -1) this.todos.splice(index, 1);
}
});
const TodoItem = snippet<{ todo: Todo }>(({ todo }) => (
<li>
<span>{todo.text}</span>
<button onClick={() => state.remove(todo)}>Remove</button>
</li>
), 'TodoItem');
return render(() => (
<ul>
{state.todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</ul>
));
});5. Bi-Directional Component
- Use for: Generic components (inputs, selects, etc.) that support both two-way and one-way bindings.
- Behavior: Component read and write to props, props propagates to parent component if it's a binding reference.
export const TextInput = setup<TextInputProps>((props) => {
const handleChange = (e) => {
props.value = e.target.value;
props.onChange?.(e.target.value);
};
return render(() => (
<input
type="text"
value={props.value ?? ''}
onChange={handleChange}
/>
));
});6. Optimistic UI
- Use for: UI updates that are optimistic and can be undone.
export const LikeButton = setup<{ liked: boolean }>((props) => {
const handleClick = () => {
const [rollback, settled] = undoable(() => {
props.liked = !props.liked;
});
updateLike(props.liked).then(settled).catch(rollback);
};
return render(() => (
<button onClick={handleClick}>{props.liked ? 'Unlike' : 'Like'}</button>
));
});Reactivity System
1. Effects
- Use for: Side effects that respond to state changes
- Pattern: Automatic dependency tracking, no arrays
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:
effect(() => {
const content = doc.content; // Tracked
const endpoint = untrack(() => settings.saveUrl); // Not tracked
untrack(() => {
fetch(endpoint, { body: content }); // No tracking inside
});
});Snapshots:
effect(() => {
if (state.query) {
const copy = snapshot(state); // Deep clone, not reactive
const json = JSON.stringify(copy); // Safe
}
});2. Subscribe (Global Observability)
Use for: Listening to any change in an object
subscribe(user, (val, event) => {
console.log('Changed:', event);
}, true); // recursive = true (default)
// vs effect:
// - effect: Granular, runs immediately
// - subscribe: Global, runs only on updatesLifecycle
onMount
- Use for: DOM access, 3rd-party libs, animations
- Runs: Once after component is in DOM
onMount(() => {
inputRef.current?.focus();
// Return cleanup
return () => console.log('Cleanup');
});onCleanup
- Use for: Cleanup when component unmounts
onCleanup(() => {
window.removeEventListener('resize', handleResize);
});Lifecycle Flow:
- Setup → 2. Render → 3. Mount → 4.
onMount→ 5. Updates → 6. Unmount → 7.onCleanup
Binding & Refs
Binding is a core feature of Anchor for preventing unnecessary re-renders. By default, passing props in React is pass-by-value, which requires the parent to re-render whenever the value changes. Anchor's $bind() and $use() enable pass-by-reference, allowing child components to update reactively without parent re-renders.
When to use:
- ✅ Always prefer
$bind()or$use()when passing reactive state to child components - ✅ Use
$bind()for two-way data flow (child can read and write) - ✅ Use
$use()for one-way data flow (child can only read) - ❌ Avoid pass-by-value (
value={state.value}) for reactive state—it forces parent re-renders
1. Two-Way Binding ($bind(source, key?))
- Use for: Two-way data binding between parent and child components
- Behavior: Pass-by-reference, child can read and write to parent's state
- Pattern: Use
$bind()in parent, declareBindable<T>type in child props
Parent (passing binding):
import { $bind, mutable } from '@anchorlib/react';
const count = mutable(0);
const state = mutable({ count: 0 });
// Binding to MutableRef
<Counter value={$bind(count)} />
// Binding to specific key
<Counter value={$bind(state, 'count')} />Child (accepting binding):
import { type Bindable, setup, render } from '@anchorlib/react';
export type CounterProps = {
value: Bindable<number>; // Declares this prop accepts two-way binding
};
export const Counter = setup<CounterProps>((props) => {
const increment = () => props.value++; // Updates parent!
return render(() => <button onClick={increment}>{props.value}</button>);
});Advanced: Syncing with Internal State
import { type Bindable, effect, mutable, setup } from '@anchorlib/react';
export type TabProps = {
value?: Bindable<string>;
disabled?: boolean;
};
export const Tab = setup<TabProps>((props) => {
const tab = mutable({ active: '', disabled: false });
// Sync props to internal state
effect(() => (tab.active = props.value ?? ''));
effect(() => (tab.disabled = props.disabled ?? false));
// Sync internal state back to props (two-way binding)
effect(() => (props.value = tab.active));
// ... rest of component
});2. One-Way Binding ($use(source, key?))
- Use for: Pass-by-reference for reactive values (avoids parent re-renders)
- Behavior: Child receives a reference to parent's state and updates reactively
- Pattern: Use
$use()when child only needs to read the value (one-way data flow)
Why use $use()?
Without $use(), you're passing by value, which requires parent re-render:
// ❌ Pass-by-value: Parent must re-render to update child
<Tab disabled={state.disabled} />
// ✅ Pass-by-reference: Child updates reactively without parent re-render
<Tab disabled={$use(state, 'disabled')} />Examples:
import { $use, $bind, mutable } from '@anchorlib/react';
const tabs = mutable({
active: 'profile',
disabled: false,
});
<Tab
value={$bind(tabs, 'active')} // Two-way: Tab can change active tab
disabled={$use(tabs, 'disabled')} // One-way: Tab reactively reads disabled state
/>
// Or with functions (computed values)
<AdminForm isAdmin={$use(() => user.role === 'admin')} />3. DOM Refs
Access DOM:
const inputRef = nodeRef<HTMLInputElement>((node) => {
if (node) node.focus();
});
return render(() => <input ref={inputRef} />);Reactive Attributes (bypasses React):
const panelRef = nodeRef(() => ({
className: state.activeTab === 'home' ? 'active' : 'hidden',
'aria-hidden': state.activeTab !== 'home'
}));
return render(() => (
<div ref={panelRef} {...panelRef.attributes}>
<HeavyContent /> {/* Won't re-render when class changes */}
</div>
));When to use nodeRef:
- ✅ Containers with large trees (avoid re-rendering children)
- ✅ High-frequency updates (animations, drag-and-drop)
- ❌ Simple leaf components (use standard JSX)
Advanced Patterns
1. Async Handling (query)
Anchor provides a robust query() primitive for handling asynchronous operations. It solves common problems like race conditions, status tracking, and cancellation.
Features:
- Auto-Status:
statusproperty ('pending' | 'success' | 'error') - Auto-Cancellation: Aborts previous request if a new one starts
- Manual Cancellation: Call
.abort()to cancel ongoing request - Signal Passing: Passes
AbortSignalto your async function
const userQuery = query(async (signal) => {
const res = await fetch('/api/user', { signal });
return res.json();
}, { name: 'Guest' }); // Initial data (safe access)
// Manual abort (e.g., on unmount)
onCleanup(() => userQuery.abort());
// Usage in View
if (userQuery.status === 'pending') return <Spinner />;
if (userQuery.status === 'error') return <Error msg={userQuery.error.message} />;
return <Profile data={userQuery.data} />;2. State Persistence (persistent & session)
Don't manually sync with localStorage or sessionStorage. Use Anchor's reactive storage primitives. They automatically:
- Load initial state from storage
- Sync changes to storage
- Update specific properties reactively
import { persistent, session } from '@anchorlib/react/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: ''
});3. Optimistic UI (undoable)
For interactions that require instant feedback (like "Like" buttons), use undoable(). It provides a way to optimistically update the state and automatically rollback if the server request fails.
const [rollback, settled] = undoable(() => {
// 1. Optimistic update (runs immediately)
state.liked = !state.liked;
});
// 2. Perform actual request
api.like()
.then(settled) // 3a. Success: confirm change
.catch(rollback); // 3b. Error: revert change automatically4. Advanced Reactivity
Control how and when reactivity triggers track dependencies.
untrack(fn): Executesfnwithout tracking dependencies. Essential for reading state inside an effect without subscribing to it.snapshot(state): Creates a deep, non-reactive copy of the state. Use this before passing state toJSON.stringify()to avoid performance issues.subscribe(state, callback): Manually listen to all changes on an object. Useful for logging or debugging.
5. Lifecycle Deep Dive: effect() vs onMount()
Understanding the difference is critical for proper data fetching and initialization.
| Feature | effect() | onMount() |
|---|---|---|
| Timing | Runs IMMEDIATELY during setup (before mount) | Runs ONCE after component is mounted in DOM |
| Tracking | Auto-tracks any state read synchronously | No tracking. Safe to read state without re-running |
| Re-runs | Yes, whenever dependencies change | No, never re-runs |
| Best For | Syncing state A to B, derived logic | Data fetching, DOM manipulation, extensive setup |
TypeScript
// Component with props
interface Props {
value: number;
onChange?: (val: number) => void;
}
export const Counter = setup<Props>((props) => {
// Can destructure in render()
return render<typeof props>(({ value }) => (
<div>{value}</div>
));
});
// Component with two-way binding
interface TextInputProps {
value: Bindable<string>; // Two-way binding
placeholder?: string;
}
export const TextInput = setup<TextInputProps>((props) => {
const handleChange = (e) => props.value = e.target.value;
return render(() => (
<input
value={props.value}
placeholder={props.placeholder}
onChange={handleChange}
/>
));
});
// Template with props
const Button = template<{ variant: 'primary' | 'secondary' }>(
({ variant }) => <button className={`btn-${variant}`} />,
'Button'
);
// Snippet with props
const Item = snippet<{ text: string }>(
({ text }, parentProps) => <li>{text}</li>,
'Item'
);
// Mutable with type
const state = mutable<{ count: number }>({ count: 0 });
// NodeRef with type
const inputRef = nodeRef<HTMLInputElement>();Best Practices
1. Logic-Driven Design
Pattern: Group data + behavior in objects
// ❌ State-Driven
const [newText, setNewText] = useState('');
const [todos, setTodos] = useState([]);
const addTodo = () => {
setTodos([...todos, { text: newText }]);
setNewText('');
};// ✅ Logic-Driven
const todoApp = mutable({
newText: '',
todos: [],
addTodo() {
this.todos.push({ text: this.newText });
this.newText = '';
}
});2. Stable Scopes
Pattern: Functions created once, always current
// ❌ State-Driven
const increment = () => setCount(count + 1); // Recreated every render
const stableIncrement = useCallback(() => setCount(c => c + 1), []); // Needs useCallback// ✅ Logic-Driven
const increment = () => state.count++; // Created once, always current3. Surgical Mutations
Pattern: Direct property updates
// ❌ State-Driven
setUser({ ...user, age: user.age + 1 });// ✅ Logic-Driven
user.age++;4. Automatic Tracking
Pattern: No dependency arrays
// ❌ State-Driven
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]); // Manual deps// ✅ Logic-Driven
effect(() => {
fetchUser(state.userId).then(d => state.data = d);
}); // Auto-tracked5. Efficient Views
Pattern: Snippets instead of component splitting
// ❌ State-Driven (props drilling)
function UserCard({ user }) {
return (
<div>
<UserHeader name={user.name} />
<UserBody role={user.role} />
</div>
);
}// ✅ Logic-Driven (snippets)
export const UserCard = setup(() => {
const user = mutable({ name: 'John', role: 'Admin' });
const Header = snippet(() => <h1>{user.name}</h1>);
const Body = snippet(() => <p>{user.role}</p>);
return (
<div>
<Header /> {/* Updates only when user.name changes */}
<Body /> {/* Updates only when user.role changes */}
</div>
);
});6. Component for Bi-Directional Data Flow
// ❌ Parent needs to re-render to get the updated value
const TextInput = template<{ value, onChange }>((props) => (
<input value={props.value} onChange={props.onChange} />
));// ✅ Component updates itself when the value changes
const TextInput = setup<{ value: Bindable<string>, onChange?: (e: Event) => void }>((props) => {
const handleChange = (e) => {
props.value = e.currentTarget.value;
props.onChange?.(e);
};
return render(() => (
<input value={props.value} onChange={handleChange} />
));
});7. Reusable Component for Generic Views
// ❌ Too verbose for similar functionalities
const SignUp = setup(() => {
const state = mutable({
email: '',
password: '',
});
const handleEmailChange = (e) => state.email = e.currentTarget.value;
const handlePasswordChange = (e) => state.password = e.currentTarget.value;
const EmailInput = snippet(() => (
<input value={state.email} onChange={handleEmailChange} />
));
const PasswordInput = snippet(() => (
<input type="password" value={state.password} onChange={handlePasswordChange} />
));
return (
<form>
<EmailInput />
<PasswordInput />
<button type="submit">Sign Up</button>
</form>
);
});// ✅ Component with static layout.
const SignUp = setup(() => {
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>
);
});
// ✅ TextInput updates itself when the value changes
const TextInput = setup<{ value: Bindable<string>; type?: string }>((props) => {
return render(() => (
<input type={props.type ?? 'text'} value={props.value} onChange={(e) => props.value = e.currentTarget.value} />
));
});7. Event-Driven Logic
Pattern: Direct function calls, not effects
// ❌ State-Driven
const [shouldSave, setShouldSave] = useState(false);
const handleSubmit = () => setShouldSave(true);
useEffect(() => {
if (shouldSave) {
saveData();
setShouldSave(false);
}
}, [shouldSave]);// ✅ Logic-Driven
const handleSubmit = () => saveData(state.data);8. Computed Properties
Pattern: JavaScript getters
// ❌ State-Driven
const total = useMemo(() => price * quantity, [price, quantity]);// ✅ Logic-Driven
const cart = mutable({
price: 10,
quantity: 2,
get total() { return this.price * this.quantity; }
});Universal Components (RSC/SSR/CSR)
Anchor components work seamlessly across all rendering modes:
export const UserProfile = setup<ProfileProps>(({ id, user }) => {
const state = mutable<UserState>({
user: user || null, // If provided (RSC), start loaded
loading: !user,
error: null
});
const getUser = () => {
if (!id) return;
state.loading = true;
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => {
state.user = data;
state.loading = false;
});
};
onMount(() => {
if (!state.user && id) getUser();
});
const Content = snippet(() => (
<div>
<h1>{state.user?.name}</h1>
{id && <button onClick={callback(getUser)}>Refresh</button>}
</div>
));
return render(() => {
if (state.loading) return <div>Loading...</div>;
if (state.error) return <div>{state.error}</div>;
return <Content />;
});
});Usage:
- RSC:
<UserProfile user={user} />- Static HTML, zero JS - SSR:
<UserProfile user={user} id={user.id} />- HTML + hydration - CSR:
<UserProfile id={1} />- Client-side fetch
Migration Strategy
1. Problem Identification
Identify "hot paths" - high-frequency updates causing re-render cascades
2. Gradual Migration (Hybrid)
Isolate hot paths with mutable + snippet:
export const TodoApp = () => {
const [todos, setTodos] = useState([]);
const formState = mutable({ text: '' }); // Bypass React
const TodoForm = snippet(() => ( // Update boundary
<input
value={formState.text}
onInput={e => formState.text = e.target.value}
/>
));
return (
<div>
<TodoForm /> {/* No longer triggers TodoApp re-render */}
<ul>{todos.map(...)}</ul>
</div>
);
};3. Full Migration
Convert to setup() for complete stability:
export const TodoApp = setup(() => {
const formState = mutable({ text: '' });
const todos = mutable([]);
const handleSubmit = () => {
todos.push({ text: formState.text });
formState.text = '';
};
const TodoForm = snippet(() => (
<input
value={formState.text}
onInput={e => formState.text = e.target.value}
/>
));
const TodoList = snippet(() => (
<ul>{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}</ul>
));
return (
<div>
<TodoForm />
<TodoList />
</div>
);
});Common Pitfalls
❌ Destructuring Props
const { name } = props; // Captures initial value only❌ Spreading Props in Setup
const divProps = { ...props }; // Error! Use props.$omit([...])❌ Accessing State in Static JSX
return <div>{state.count}</div>; // Shows initial value, never updates
// Fix: Wrap in render() or use snippet❌ Using React Hooks in Setup
const [count, setCount] = useState(0); // Breaks stable logic model❌ Binding to Immutable State
const state = immutable({ count: 0 });
<Counter value={bind(state, 'count')} /> // Error!Quick Reference
Imports
import {
// State
mutable, immutable, writable, derived,
// Component
setup, render,
// View
template, snippet,
// Reactivity
effect, untrack, snapshot, subscribe,
// Lifecycle
onMount, onCleanup,
// Binding
$bind, $use, nodeRef, callback
} from '@anchorlib/react';Client Initialization
// main.tsx or app/layout.tsx
import '@anchorlib/react/client'; // ⚠️ Required for reactivityDecision Trees
State Type:
- Local component state →
mutable - Shared state →
immutable+writable - Computed value (same object) → getter
- Computed value (cross-object) →
derived
View Type:
- Primary output → Component View (
render()) - Reusable, props-driven → Template
- Component-specific, needs scope → Snippet
- Never changes → Static JSX
When to Use:
effect→ Reactive side effectsonMount→ DOM access after mountonCleanup→ Cleanup on unmountnodeRef→ High-frequency DOM updates$bind()→ Two-way binding (pass-by-reference)$use()→ One-way binding (pass-by-reference)untrack→ Read without subscribing
Performance Optimization
- Use Templates for lists - Independent item updates
- Use Snippets to avoid props drilling - No performance penalty for large components
- Use
nodeReffor containers - Avoid re-rendering children - Use getters for derived state - Auto-cached
- Use
untrackfor expensive reads - Avoid over-subscription - Use Static JSX for layouts - Zero re-render overhead
FAQ Quick Answers
Q: Does Anchor replace React? A: No. It's an enhancement layer. React handles rendering, Anchor handles logic stability and fine-grained reactivity.
Q: Doesn't mutation break React? A: No. Anchor uses the Gateway Pattern - mutations are intercepted and trigger only affected Views.
Q: Is it hard to learn? A: No. Uses standard JavaScript (objects, functions) instead of complex hooks.
Q: Performance overhead? A: Minimal. Net gain from stopping re-render cascades vastly outweighs library cost.
Q: When does it bypass React? A: Only with nodeRef for high-frequency updates (animations, etc.).
Q: Can I use React hooks in setup? A: You can, but shouldn't. Breaks the stable logic model.
Q: How are props different? A: Props are reactive proxies. Never destructure in setup body. Can destructure in Views (render/template/snippet) since they're reactive boundaries.
Q: Do I need dependency arrays? A: No. Effects track automatically based on what you read.
Key Principles for AI
- Always initialize client:
import '@anchorlib/react/client'first - Never destructure props in setup body (can destructure in Views)
- Use
$omit()/$pick()for rest props in setup body (can use spread in Views) - Wrap reactive JSX in
render(),template(), orsnippet() - Prefer
immutable+writablefor shared state - Use getters for derived state within objects
- Use
derived()for cross-object computations - Always name components and views - Pass name as second argument to
setup(),render(),template(), andsnippet()for better React DevTools visibility - Extract list items to separate views (template or snippet)
- Use
query()for async operations - Use
persistent()orsession()for browser storage
Example naming:
// Component with named view
const Tab = setup(() => {
return render(() => <div>Tab</div>, 'Tab');
}, 'Tab');
// Template
const TodoItem = template(({ todo }) => <li>{todo.text}</li>, 'TodoItem');
// Snippet
const Header = snippet(() => <h1>Title</h1>, 'Header');React DevTools tree:
- Tab (Setup)
- Tab (View)
- TodoItem (Template)
- Header (Snippet)Success Checklist
- [ ] Client initialized (
import '@anchorlib/react/client') - [ ] Component uses
setup()not function component - [ ] State uses
mutable()orimmutable() - [ ] Props never destructured in setup body (OK in Views)
- [ ] Rest props use
$omit/$picknot spread - [ ] Reactive JSX wrapped in
render(),template(), orsnippet() - [ ] All components and views have names (second argument to setup/render/template/snippet)
- [ ] List items extracted to separate views
- [ ] Effects use
effect()notuseEffect - [ ] No React hooks in setup
- [ ] Shared state uses
immutable+writable - [ ] Computed values use getters or
derived() - [ ] Cleanup handlers defined for side effects
- [ ] Use
query()for async operations - [ ] Use
persistent()orsession()for state persistence - [ ] Use
undoable()for optimistic UI - [ ] Use
nodeReffor high-frequency updates