Migration Guide
Migrating to Anchor is an architectural shift from Top-Down Rendering (React) to Signal-Based Fine-Grained Reactivity (Anchor). This guide demonstrates the strategic steps to adopt this model.
Here is a standard React implementation of a Todo list.
// TodoApp.tsx
import { useState } from 'react';
export const TodoApp = () => {
const [text, setText] = useState('');
const [todos, setTodos] = useState([]);
const handleSubmit = () => {
setTodos([...todos, { text }]);
setText('');
};
return (
<div>
<div className="form">
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={handleSubmit}>Add</button>
</div>
<ul>
{todos.map(todo => <li key={todo.text}>{todo.text}</li>)}
</ul>
</div>
);
};1. Problem Identification
Before migrating, we must analyze why this component needs migration.
- The
textstate is linked to theTodoAppcomponent scope. - Typing in the
<input>triggerssetText, which forces the entire function to re-run. - The
<ul>list (which hasn't changed) is forced to re-render and Diff against the DOM on every single keystroke.
2. Setting Priority (Strategy)
Now we decide how to attack the problem. We use the "Hot-Path" Strategy.
- High Priority (Hot Path): The Form. It updates frequently (on input) and causes the lag. Actions: Isolate this first.
- Low Priority (Cold Path): The List. It updates rarely (on submit). Actions: Inherit existing behavior or migrate later.
Decided Approach: We will first isolate the form using a Hybrid Integration to stop the bleeding, then perform a Full Migration for long-term stability.
3. Gradual Migration
Goal: Isolate the "Hot Path" (Form) immediately.
We create an Update Boundary to isolate the high-frequency state changes. This keeps high-frequency state changes contained, preventing them from leaking out and triggering the parent React component.
import { useState } from 'react';
import { mutable, snippet } from '@anchorlib/react';
export const TodoApp = () => {
const [todos, setTodos] = useState([]);
// STEP 1: Bypass React Render Cycle
// We move the high-frequency state to a mutable signal.
const formState = mutable({ text: '' });
const handleSubmit = () => {
setTodos([...todos, { text: formState.text }]);
formState.text = '';
};
// STEP 2: Create Update Boundary
// We wrap the input UI so it listens strictly to the signal, ignoring React updates.
const TodoForm = snippet(() => (
<div className="form">
<input
value={formState.text}
onInput={e => formState.text = e.target.value}
/>
<button onClick={handleSubmit}>Add</button>
</div>
));
return (
<div>
{/* Updates here NO LONGER trigger TodoApp re-render */}
<TodoForm />
<ul>
{todos.map(todo => <li key={todo.text}>{todo.text}</li>)}
</ul>
</div>
);
};4. Full Migration
Goal: Complete stability and granular reactivity for the entire component.
Now that the immediate bottleneck is solved, we adopt the Constructor Pattern. This ensures our logic initializes exactly once, and every part of the UI updates independently.
import { setup, template, snippet, mutable } from '@anchorlib/react';
// STEP 1: Initialize Once
// Logic moves to 'setup', guaranteeing it never re-runs.
export const TodoApp = setup(() => {
const formState = mutable({ text: '' });
const todos = mutable([]);
const handleSubmit = () => {
todos.push({ text: formState.text });
formState.text = '';
};
// STEP 2: Granular View Definitions
// We define views that have access to the closure scope.
const TodoForm = snippet(() => (
<div className="form">
<input
value={formState.text}
onInput={e => formState.text = e.target.value}
/>
<button onClick={handleSubmit}>Add</button>
</div>
));
const TodoList = snippet(() => (
<ul>
{todos.map(todo => <TodoItem key={todo.text} todo={todo} />)}
</ul>
));
return (
<div>
<TodoForm />
<TodoList />
</div>
);
});
// STEP 3: External Component Definition
// Pure views are defined outside to maximize reusability.
const TodoItem = template(({ todo }) => (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todo.completed = !todo.completed}
/>
{todo.text}
</li>
));Recap
By following this process, we have transformed the component architecture:
- Identified that the "Hot Path" (Input) was dragging down the entire app.
- Prioritized fixing the input first using a Hybrid Strategy.
- Executed a Full Migration to achieve:
- Stable Logic: The component function only runs once (
setup). - Zero Re-renders: The parent container never updates after mount.
- Fine-Grained Updates: Typing only updates the Input DOM node; Adding a todo only appends a DOM node.
- Stable Logic: The component function only runs once (
Advanced Migration
This section covers advanced scenarios you may encounter when integrating Anchor into an existing React codebase.
Mixing React's State and Anchor's State
When you need to use both React's state (useState) and Anchor's state (mutable) in the same component scope, you must preserve Anchor's state across React re-renders.
Standalone State
If your Anchor state is independent, wrap it in useState to preserve it:
import { useState } from 'react';
import { mutable, snippet } from '@anchorlib/react';
const Counter = () => {
// React state
const [count, setCount] = useState(0);
// Anchor state - preserved across React re-renders
const [counter] = useState(() => mutable(0));
const AnchorCounter = template(() => (
<button onClick={() => counter.value++}>{counter.value}</button>
));
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<AnchorCounter />
</>
);
};Dependent State
If your Anchor state depends on React state, use useMemo to recreate it when dependencies change:
import { useState, useMemo } from 'react';
import { mutable, template } from '@anchorlib/react';
const Counter = () => {
const [initialValue, setInitialValue] = useState(0);
// Anchor state - recreated when initialValue changes
const counter = useMemo(() => mutable(initialValue), [initialValue]);
const AnchorCounter = template(() => (
<button onClick={() => counter.value++}>{counter.value}</button>
));
return (
<>
<button onClick={() => setInitialValue(v => v + 10)}>
Reset to {initialValue + 10}
</button>
<AnchorCounter />
</>
);
};WARNING
Mixing React and Anchor state is not recommended. You lose the benefits of Anchor's stable scope and universality. This pattern should only be used as a temporary workaround during gradual migration.
Using Anchor Components in React Components
Anchor components and templates integrate seamlessly into standard React's component tree. The Anchor component maintains its stable logic scope while receiving props from the React parent.
import { useState } from 'react';
import { setup, render, mutable } from '@anchorlib/react';
// Stable component with internal state
const Counter = setup<{ onIncrement?: () => void }>((props) => {
const state = mutable({ count: 0 });
const increment = () => {
state.count++;
props.onIncrement?.();
};
return render(() => (
<button onClick={increment}>{state.count}</button>
));
});
// React component
const ReactApp = () => {
const [total, setTotal] = useState(0);
return (
<div>
<p>Total clicks: {total}</p>
<Counter onIncrement={() => setTotal(t => t + 1)} />
</div>
);
};Using React Components in Anchor Components
Third-party React components (Shadcn/UI, Material-UI, etc.) work seamlessly inside Anchor components.
import { setup, mutable, snippet } from '@anchorlib/react';
import { Badge } from '@/components/ui/badge'; // Shadcn/UI component
import { Button } from '@/components/ui/button';
const Dashboard = setup(() => {
const state = mutable({
notifications: 5,
messages: 0
});
// Only the dynamic badges are in a snippet
const NotificationBadge = snippet(() => (
<Badge variant="destructive">{state.notifications}</Badge>
));
const MessageBadge = snippet(() => (
<Badge variant="secondary">{state.messages}</Badge>
));
return (
<div>
<div>
<span>Notifications</span>
<NotificationBadge />
</div>
<div>
<span>Messages</span>
<MessageBadge />
</div>
<Button onClick={() => state.notifications++}>
Add Notification
</Button>
<Button onClick={() => state.messages++}>
Add Message
</Button>
</div>
);
});In the example above, each Badge is isolated in its own snippet, so they only update when their specific state changes.
Important Considerations:
- Most component libraries (like Shadcn/UI) are already optimized with
React.memointernally, but not all are. - Third-party components can't receive binding reference, so you need to put them in a reactive boundary (
template,snippet, orrender) and use standard imperative handling.
// ❌ Won't work because component will receive a binding reference object, not the actual value.
return (
<MaterialInput value={bind(state, 'name')} />
);// ✅ Works because component will receive the actual value.
render(() => (
<MaterialInput value={state.name} onChange={e => state.name = e.target.value} />
));- If you notice performance issues with unoptimized third-party components, you can wrap them:
import { memo } from 'react';
import { Badge as ShadcnBadge } from '@/components/ui/badge';
// Wrap if the library component isn't already memoized
const Badge = memo(ShadcnBadge);- For frequently-used components, consider creating Anchor component for better performance and consistency with Anchor's reactivity model.