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
State binding allows you to synchronize your component's internal state with any external reactive source (props, global state, or other signals).
1. One-Way Synchronization
To sync internal state with External State (props, global state, or other signals), use an effect. This ensures your component reacts to changes from the outside world while maintaining its own local logic.
export const Tab = setup((props: { value?: string }) => {
const state = mutable({ active: '' });
// Sync: When props.value changes, update internal state
effect(() => {
if (props.value !== undefined) {
state.active = props.value;
}
});
return render(() => <div>{state.active}</div>);
});2. Two-Way Binding
Anchor supports true two-way binding between components. If a parent passes a Binding Reference, the child can update the parent's state by simply assigning to the prop.
Parent Component
Use the bind() helper to pass a reference to a mutable property.
import { setup, mutable, bind, render } from '@anchorlib/react';
import { Counter } from './Counter';
export const App = setup(() => {
const state = mutable({ count: 0 });
return render(() => (
// Pass a binding to 'state.count'
<Counter value={bind(state, 'count')} />
));
});Child Component
The child component doesn't need to know it received a binding. It just assigns to the prop.
export const Counter = setup((props: { value: number }) => {
const increment = () => {
// If 'value' is a binding, this updates the PARENT's state!
props.value++;
};
return render(() => (
<button onClick={increment}>
Count: {props.value}
</button>
));
});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. These update the DOM directly when state changes, bypassing the React render cycle for high performance.
// Ideally used for containers to avoid re-rendering children
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.
Creating Binding References
You can create a Binding Reference that can be passed to components for two-way binding.
It supports two types of state:
- Mutable Object:
bind(object, key) - Mutable Ref:
bind(ref)
const state = mutable({ text: '' });
const count = mutable(0);
// 1. Bind to object property
<TextInput value={bind(state, 'text')} />
// 2. Bind to mutable ref directly
<Counter value={bind(count)} />Bindable Interfaces
You can create a bindable state that stays synchronized with a source object (like props). This is useful for creating components that can be both controlled and uncontrolled, or simply to normalize props into a mutable interface.
import { setup, bindable, render } from '@anchorlib/react';
export const TextInput = setup((props: { value?: string }) => {
// 1. Create a local ref 'text'
// 2. Initialize it with '' (fallback if props.value is undefined)
// 3. Sync it with props.value
const text = bindable('', props, 'value');
return render(() => (
<input
value={text.value}
onInput={(e) => text.value = e.currentTarget.value}
/>
));
});In this example:
- If
props.valuechanges (parent updates),text.valueupdates automatically. - If user types,
text.valueupdates, which propagates toprops.value.- If
props.valuewas passed viabind(), the parent state updates! - If
props.valuewas a plain string, it just updates the local prop proxy (no-op for parent).
- If
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={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.