Binding & Refs
In Anchor, "Binding" refers to two things:
- State Binding: Synchronizing state between components (Parent <-> Child).
- DOM Binding: Synchronizing state with DOM elements (Attributes, Events).
State Binding
There are two fundamental ways to pass data to components:
Pass-by-Value
When you pass a value directly, the child component receives a copy of the value. If the state changes, the parent must re-render to pass the new value.
<Input value={state.value} />This requires the parent view to re-render every time state.value changes to provide the new value to the Input component.
Pass-by-Reference
When you use Anchor's binding, the child component receives a reference to the state. The component can read changes directly without requiring the parent to re-render.
<Input value={$use(state, 'value')} />This is pass-by-reference. The Input component reads a reference, so it can self-update when the value changes. The parent can stay static and doesn't need to re-render.
Tips
If the key is 'value', it can be omitted: $use(state) is equivalent to $use(state, 'value').
Important!
Binding only works with Components (created with setup()), Templates (created with template()), and Snippets (created with snippet()).
One-Way Binding
Use $use() to create a one-way data binding. Updates in the state will be propagated to the prop, but changes to the prop won't affect the original state.
import { setup, mutable, $use } from '@anchorlib/react';
import { Display } from './Display';
export const App = setup(() => {
const state = mutable({ count: 0 });
return (
<div>
<button onClick={() => state.count++}>Increment</button>
<Display value={$use(state, 'count')} />
</div>
);
});The child component receives the value and can update itself when it changes:
import { template } from '@anchorlib/react';
export const Display = template<{ value: number }>(({ value }) => (
<div>Current count: {value}</div>
));Two-Way Binding
In Anchor, components should enhance native element behavior. Native HTML elements like <input> are smart—they behave autonomously, showing and updating values as users interact with them, regardless of whether you handle their events.
When you design a component that displays and updates a value, it should work independently. The component manages its own behavior, and parent binding is just a side-effect that allows the parent to react to the component's behavior.
To create two-way data binding, use $bind(). It keeps state and prop in sync—updates on either side are propagated to each other.
Typing Components for Two-Way Binding
For TypeScript to recognize that a component accepts two-way binding with $bind(), you must explicitly type the property with Bindable<type>:
import type { Bindable } from '@anchorlib/react';
type CounterProps = {
value: Bindable<number>; // ✅ Explicitly typed as Bindable
onChange?: (value: number) => void;
};
export const Counter = setup<CounterProps>((props) => {
const increment = () => {
props.value++;
props.onChange?.(props.value);
};
return render(() => (
<button onClick={ increment }>
Count: { props.value }
</button>
));
});This component works autonomously with all binding patterns:
// ✅ One-way pass-by-value
render(() => <Counter value={state.count} />)
// ✅ One-way pass-by-reference
<Counter value={$use(state, 'count')} />
// ✅ Two-way binding
<Counter value={$bind(state, 'count')} />
// ✅ Two-way binding + imperative handling
<Counter value={$bind(state, 'count')} onChange={handleChange} />Try it Yourself
Why Binding Matters?
Native HTML Input requires imperative handling with boilerplate code that's error-prone:
const input = document.querySelector('input');
input.value = user.name;
input.addEventListener('input', (e) => user.name = e.target.value);React's Controlled Input doesn't fix the native's problem—it makes it worse with more boilerplate and is even more error-prone:
<input value={state.correctProp} onChange={(e) => setWrongProp(e.target.value)} />This won't tell you there's an error. User can't type in the input because the value never changes. You have to debug it carefully. If the onChange is wrapped in a separate function, you need to scroll to check what it does inside.
Anchor's $bind() provides clear intent:
<Input value={$bind(user, 'name')} onChange={validateName} />$bind(user, 'name')immediately tells you it reads and writes touser.name- If you pass the wrong prop, you'll see it immediately because it shows the wrong value before you even make a change
- Binding + imperative handling gives clear intent, separating two-way binding from side-effects
DOM Binding
DOM Binding is used to bind state to DOM elements. It handles both accessing the element and efficiently updating its attributes.
Accessing DOM Elements
Use nodeRef to get a handle on a DOM node. You can access the node directly inside the factory function when it becomes available.
const inputRef = nodeRef<HTMLInputElement>((node) => {
// This runs when the node is mounted
if (node) node.focus();
});
return render(() => <input ref={inputRef} />);Reactive Attributes
Pass a factory function to nodeRef to create Reactive Attributes. This is where nodeRef shines: you write declarative, reactive code that automatically updates the DOM when state changes, but it bypasses React's render cycle for direct DOM manipulation. You get the best of both worlds—declarative syntax with imperative performance.
// Declarative: You describe WHAT you want
// Reactive: Automatically updates when state changes
// Performant: Direct DOM updates, no React re-render
const panelRef = nodeRef(() => ({
className: state.activeTab === 'home' ? 'active' : 'hidden',
'aria-hidden': state.activeTab !== 'home'
}));
return render(() => (
// Changing class/attributes here won't re-render <HeavyContent />
<div ref={panelRef} {...panelRef.attributes}>
<HeavyContent />
</div>
));Event Handlers in nodeRef
You can include event handlers in the nodeRef factory, but they are only used for initial hydration by React. They are ignored during reactive updates.
const btnRef = nodeRef(() => ({
className: state.active ? 'active' : '',
onClick: () => console.log('Clicked') // Passed to React via {...btnRef.attributes}
}));
// The onClick is static. Changing it later in the factory won't update the listener.TIP
Event handlers in nodeRef are safe for React Server Components (RSC) because they are automatically stripped out during server rendering.
Try it Yourself
Creating Binding References
Anchor provides two functions for creating binding references:
$use() - One-Way Binding
Creates a one-way binding where updates in the state are propagated to the prop.
const state = mutable({ text: '' });
const count = mutable(0);
// 1. One-way bind to object property
<Display value={$use(state, 'text')} />
// 2. One-way bind to mutable ref directly
<Display value={$use(count)} />$bind() - Two-Way Binding
Creates a two-way binding where state and prop are kept in sync.
const state = mutable({ text: '' });
const count = mutable(0);
// 1. Two-way bind to object property
<TextInput value={$bind(state, 'text')} />
// 2. Two-way bind to mutable ref directly
<Counter value={$bind(count)} />Best Practices
Avoid Binding to Immutable State
Never use $bind() with an immutable state object. While Anchor will detect this and warn you, it's a bad practice.
const state = immutable({ count: 0 });
// ❌ WRONG: Cannot bind to immutable state
<Counter value={$bind(state, 'count')} />If you need to share state that can be updated by children, use mutable or provide a specific writable contract.
Prefer One-Way for Complex Logic
Two-way binding ($bind()) is excellent for form inputs and simple settings. However, for complex business logic, explicit event handlers (One-Way Data Flow) are often easier to debug and reason about.
// ✅ Good for simple inputs
<TextInput value={$bind(state, 'name')} />
// ✅ Better for complex logic
<ComplexWidget
value={$use(state, 'data')}
onChange={(newData) => {
validate(newData);
state.data = newData;
}}
/>Use nodeRef for High Frequency Updates
Because nodeRef attributes update the DOM directly (bypassing React's render cycle), they are ideal for high-frequency updates like animations, scroll positions, or drag-and-drop interactions.
// Updates style directly without re-rendering the component
const boxRef = nodeRef(() => ({
style: { transform: `translateX(${state.x}px)` }
}))When to Use nodeRef
Don't use nodeRef for everything. React's virtual DOM is fast enough for most cases.
- ✅ Use
nodeReffor Containers: If you have a component wrapping a large tree (like aTabContentorLayout), usenodeRefto toggle classes or styles. This avoids re-rendering the entire children tree just to change a class name. - ✅ Use
nodeReffor Performance: For high-frequency updates (animations, drag-and-drop). - ❌ Avoid for Leaf Components: Using
nodeReffor a simpleButtonorInputis usually overengineering. Standard JSX binding is simpler and fine for these cases.