Observation in Anchor for React
Observation is a core concept in Anchor, enabling your React components to automatically react to changes in your application's state. Anchor's fine-grained reactivity system ensures that only components or parts of components that depend on specific pieces of data re-render when that data changes, leading to highly optimized performance.
How Observation Works
When you access a property of an Anchor reactive state within a component or a reactive computation, Anchor automatically tracks that dependency. If the value of that property later changes, Anchor intelligently identifies all components that observed it and triggers a re-render for only those components, avoiding unnecessary updates to the rest of your application.
Higher-Order Components (HOCs)
These are Higher-Order Components that make React components reactive to Anchor's state changes.
observe(factory, displayName?)
A higher-order component (HOC) that creates a React component which automatically re-renders when any observable state accessed within the provided factory
callback changes.
Recommended!
This is the most recommended way to observe state changes in Anchor because it provides fine-grained reactivity - only the observed part re-renders, not the entire component where it's declared.
Params
factory
- A callback function that returns aReactNode
or a renderer factory object with lifecycle methods. This function will be executed within an observing context.displayName
(optional) - A string to be used as the display name for the returned component in React DevTools.
Returns: A new React component that is reactive to observable state changes.
Factory Object Properties
When using a factory object instead of a simple function, the following properties are supported:
name
(optional) - A string to be used as the display name for the returned component in React DevTools.render
- A function that returns aReactNode
. This function will be executed within an observing context.onMounted
(optional) - A function that is called when the component is mounted.onUpdated
(optional) - A function that is called when the component is updated due to reactive state changes.onDestroy
(optional) - A function that is called when the component is unmounted.
Usage
Basic Observation Usage
import { useAnchor } from '@anchorlib/react';
import { observe } from '@anchorlib/react/view';
const CounterManager = () => {
const [count] = useAnchor({ value: 0 });
// Create an observed component that re-renders when count.value changes
const CountDisplay = observe(() => (
<div>
<h1>Count: {count.value}</h1>
<p>This component only re-renders when count.value changes</p>
</div>
));
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
return (
<div>
<CountDisplay />
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
Using Factory Object with Lifecycle Methods
import { useAnchor } from '@anchorlib/react';
import { observe } from '@anchorlib/react/view';
const Timer = () => {
const [timer] = useAnchor({ seconds: 0 });
// Create an observed component using a factory object with lifecycle methods
const TimerDisplay = observe({
name: 'TimerDisplay',
onMounted() {
console.log('TimerDisplay mounted');
},
onUpdated() {
console.log('TimerDisplay updated');
},
onDestroy() {
console.log('TimerDisplay will be destroyed');
},
render(ref) {
// Store data on the ref for later use
if (!ref.current) {
ref.current = {
renderCount: 0,
};
}
ref.current.renderCount++;
return (
<div>
<h1>Timer: {timer.seconds}s</h1>
<p>Renders: {ref.current.renderCount}</p>
</div>
);
},
});
React.useEffect(() => {
const interval = setInterval(() => {
timer.seconds++;
}, 1000);
return () => clearInterval(interval);
}, []);
return <TimerDisplay />;
};
Using the Ref Parameter
import { useAnchor } from '@anchorlib/react';
import { observe } from '@anchorlib/react/view';
const DataList = () => {
const [data] = useAnchor({
items: ['Item 1', 'Item 2', 'Item 3'],
selected: null,
});
// The factory function receives a ref parameter
const ListView = observe((ref) => {
// Store data on the ref for later use
if (!ref.current) {
ref.current = {
lastSelected: null,
};
}
const handleSelect = (item) => {
data.selected = item;
ref.current.lastSelected = item;
};
return (
<div>
<ul>
{data.items.map((item, index) => (
<li
key={index}
onClick={() => handleSelect(item)}
style={{
backgroundColor: data.selected === item ? '#e0e0e0' : 'transparent',
fontWeight: ref.current.lastSelected === item ? 'bold' : 'normal',
}}>
{item}
</li>
))}
</ul>
<p>Last selected: {ref.current.lastSelected || 'None'}</p>
</div>
);
});
const addItem = () => {
data.items = [...data.items, `Item ${data.items.length + 1}`];
};
return (
<div>
<ListView />
<button onClick={addItem}>Add Item</button>
</div>
);
};
When to use it?
Use observe
when you want to create a reactive render function directly, often for inline rendering or when you don't need a separate component definition. It's particularly useful when you need to create components that are tightly coupled with their parent's state logic. This HOC is most suitable for selective rendering, acting as the View in the DSV pattern.
Caveat
When using the observe()
API you are not creating a component. Thus, you cannot use React hooks such as useEffect()
inside it. This API is designed to be an intuitive way to render a template and re-render when the required state changes. Its main purpose is to be used as a View
.
Additionally, when using a factory object, the render function should be pure and not have any side effects.
observable(Component, displayName?)
A Higher-Order Component (HOC) that wraps a React component to make it reactive to changes in observable state.
It automatically sets up and manages a StateObserver
instance for the wrapped component. When any observable dependencies used within the component's render phase change, the component will automatically re-render.
Params
Component
- The React component to be made observable. It should accept its original propsT
.displayName
(optional) - A string to be used as the display name for the wrapped component in React DevTools. If not provided, it will derive from the original component's display name or name.
Returns: A new React component that is reactive to observable state changes.
Usage
Wrapping an Existing Component
import { useAnchor } from '@anchorlib/react';
import { observable } from '@anchorlib/react/view';
// A regular React component
const UserCard = ({ user }) => {
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Age: {user.profile.age}</p>
</div>
);
};
// Make it observable
const ObservableUserCard = observable(UserCard);
const UserManager = () => {
const [user] = useAnchor({
name: 'John Doe',
email: 'john@example.com',
profile: {
age: 30,
},
});
const updateAge = () => {
user.profile.age++;
};
return (
<div>
{/* This component will re-render when user.profile.age changes */}
<ObservableUserCard user={user} />
<button onClick={updateAge}>Increment Age</button>
</div>
);
};
With Custom Display Name
import { useAnchor } from '@anchorlib/react';
import { observable } from '@anchorlib/react/view';
const TodoItem = ({ todo }) => {
return (
<div>
<input type="checkbox" checked={todo.completed} readOnly />
<span>{todo.text}</span>
</div>
);
};
// Wrap with a custom display name for debugging
const ObservableTodoItem = observable(TodoItem, 'ObservableTodoItem');
const TodoList = () => {
const [todos] = useAnchor([
{ id: 1, text: 'Learn Anchor', completed: false },
{ id: 2, text: 'Build an app', completed: true },
]);
const toggleTodo = (id) => {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
};
return (
<div>
{todos.map((todo) => (
<div key={todo.id} onClick={() => toggleTodo(todo.id)}>
{/* This component will re-render when todo.completed changes */}
<ObservableTodoItem todo={todo} />
</div>
))}
</div>
);
};
When to use it?
Use observable
when you have an existing React component that you want to make reactive. This HOC will re-render the wrapped component whenever there are changes to the observed states. Thus, this HOC is most suitable for use case where a full re-render is needed such as wrapping a 3rd party components, or need a simple component setup without manually declare a selective rendering.
Difference between observable()
and observe()
The key difference lies in their approach and use cases:
observable(Component)
: Wraps an existing component and is best for full component re-renders, especially when working with third-party components or when you need a simple setup without selective rendering.observe(factory)
: Creates a new component from a factory function and is best for selective rendering within the DSV pattern, where you want fine-grained control over what gets re-rendered.
bindable(Component, displayName?)
A higher-order component (HOC) that wraps a given component to enable two-way data binding between the component's input value and a bindable state.
This HOC provides automatic synchronization between the component's input value and a bindable state. It handles various input types including text, number, range, date, checkbox, and radio inputs.
Recommended!
This is the most recommended way to create form inputs that automatically synchronize with Anchor reactive state. It eliminates the need for manual event handling and state updates.
Params
Component
- The React component to be wrapped with binding functionality.displayName
(optional) - A string to be used as the display name for the resulting component in React DevTools.
Returns: A new component with binding capabilities that accepts additional props for data binding.
Props
The wrapped component accepts the following props for binding:
bind
- The bindable state object or variable reference to synchronize with.name
orbindKey
- The key of the property in the bindable state to bind to.type
- The input type which determines how values are parsed (e.g., 'number', 'date', 'checkbox').value
orchecked
- The value or checked state of the input (handled automatically when bound).onChange
- Event handler for input changes (extended with binding logic).
Supported Input Types
The bindable()
HOC automatically handles the following input types:
- Text-based inputs:
text
,password
,email
,tel
,url
,search
,color
,time
- Number-based inputs:
number
,range
(parsed as float) - Boolean inputs:
checkbox
,radio
(parsed as boolean) - Date inputs:
date
(parsed as Date object) - Other:
file
Usage
Basic Usage
import { useAnchor } from '@anchorlib/react';
import { bindable } from '@anchorlib/react/view';
// Create a bindable input component
const Input = bindable(function Input(props) {
return <input {...props} />;
});
const UserForm = () => {
const [user] = useAnchor({
name: 'John Doe',
email: 'john@example.com',
age: 30,
isActive: true,
});
return (
<form>
{/* Binding to a property by name */}
<div>
<label>Name:</label>
<Input bind={user} name="name" type="text" />
</div>
{/* Binding to a property with explicit key */}
<div>
<label>Email:</label>
<Input bind={user} bindKey="email" type="email" />
</div>
{/* Binding to a number property */}
<div>
<label>Age:</label>
<Input bind={user} name="age" type="number" />
</div>
{/* Binding to a boolean property */}
<div>
<label>Active:</label>
<Input bind={user} name="isActive" type="checkbox" />
</div>
</form>
);
};
Working with Select and Textarea
import { useAnchor } from '@anchorlib/react';
import { bindable } from '@anchorlib/react/view';
// Create bindable components for different input types
const Input = bindable(function Input(props) {
return <input {...props} />;
});
const Select = bindable(function Select(props) {
return <select {...props} />;
});
const TextArea = bindable(function TextArea(props) {
return <textarea {...props} />;
});
const ProfileForm = () => {
const [profile] = useAnchor({
username: '',
bio: '',
role: 'user',
subscribe: false,
});
return (
<form>
<div>
<label>Username:</label>
<Input bind={profile} name="username" type="text" />
</div>
<div>
<label>Bio:</label>
<TextArea bind={profile} name="bio" rows={4} />
</div>
<div>
<label>Role:</label>
<Select bind={profile} name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="moderator">Moderator</option>
</Select>
</div>
<div>
<label>Subscribe to newsletter:</label>
<Input bind={profile} name="subscribe" type="checkbox" />
</div>
</form>
);
};
Binding to Variable References
import { useAnchor, useVariable } from '@anchorlib/react';
import { bindable } from '@anchorlib/react/view';
const Input = bindable(function Input(props) {
return <input {...props} />;
});
const SearchForm = () => {
// Create a variable reference for a simple value
const [searchTerm] = useVariable('');
const [results] = useAnchor([]);
const handleSearch = () => {
// Perform search using searchTerm.value
console.log('Searching for:', searchTerm.value);
};
return (
<div>
<div>
<label>Search:</label>
{/* Bind directly to the variable reference */}
<Input bind={searchTerm} type="text" />
<button onClick={handleSearch}>Search</button>
</div>
<div>
<p>Current search term: {searchTerm.value}</p>
</div>
</div>
);
};
When to use it?
Use bindable
when you need to create form inputs that automatically synchronize with Anchor reactive state. This HOC is particularly useful for:
- Creating controlled form components with minimal boilerplate
- Handling different input types with automatic value parsing
- Reducing the need for manual event handling and state updates
- Building forms that directly interact with reactive state
Caveat
The bindable
HOC is specifically designed for form input components. It works best with components that accept standard HTML input props like value
, checked
, and onChange
. When wrapping custom components, ensure they properly handle these props.
Try It Yourself
Hook APIs
These are the primary React hooks for observing reactive state changes.
useObservedRef(observe, deps?)
Creates a reactive reference to a computed value. It automatically tracks reactive dependencies accessed within the observe function and updates the reference value when those dependencies change.
This hook is particularly useful for creating computed values that depend on multiple reactive states without manually specifying them as dependencies. The computation is automatically re-executed when any of the accessed reactive states change.
The returned ref is itself a reactive state that can be consumed by other observers or displayed in views (observable()
or observe()
).
Params
observe
- A function that computes and returns the desired value. Any reactive state accessed within this function will be automatically tracked, and the function will re-run when that state changes.deps
(optional) - An array of additional dependencies. This is useful for computation that also depends on external state such as props.
Returns: A constant reference (ConstantRef<T>
) to the computed value. The reference object remains stable, but its .value
property updates when the computed value changes.
Usage
Basic Usage
import { useAnchor } from '@anchorlib/react';
import { useObservedRef } from '@anchorlib/react';
const UserProfile = () => {
const [user] = useAnchor({
firstName: 'John',
lastName: 'Doe',
age: 30,
});
// useObservedRef tracks user.firstName and user.lastName
// The returned ref can be consumed by other observers
const fullNameRef = useObservedRef(() => {
return `${user.firstName} ${user.lastName}`;
});
// Create a component that observes the computed ref value
const FullNameDisplay = observe(() => <h1>Welcome, {fullNameRef.value}!</h1>);
const incrementAge = () => {
user.age++;
};
const changeName = () => {
user.firstName = 'Jane';
user.lastName = 'Smith'; // This will update fullNameRef.value and trigger re-render of FullNameDisplay
};
return (
<div>
<FullNameDisplay />
<p>Age: {user.age}</p>
<button onClick={incrementAge}>Increment Age</button>
<button onClick={changeName}>Change Name</button>
</div>
);
};
When to use it?
Use useObservedRef
when you need to create a computed value that can be consumed by other reactive observers. The returned ref is itself a reactive state that can be used in other computations or displayed in views.
useObserver(observe, deps?)
A custom React hook that creates a computed value by running the provided observe function within a reactive tracking context. It automatically tracks reactive dependencies accessed within the observe function and triggers re-rendering when those dependencies change.
This hook is particularly useful for creating computed values that depend on multiple reactive states without manually specifying them as dependencies. The computation is automatically re-executed when any of the accessed reactive states change.
Unlike observe() which provides fine-grained reactivity by creating a separate component, useObserver
triggers re-render of the entire component where it's declared.
Params
observe
- A function that computes and returns the desired value. Any reactive state accessed within this function will be automatically tracked, and the function will re-run when that state changes.deps
(optional) - An array of additional dependencies. This is useful for computations that also depend on external state such as props. These dependencies are used to determine when the computation should be re-executed.
Returns: The computed value returned by the observe function. This value is memoized and will only be recomputed when the tracked reactive dependencies or the additional dependencies change.
Usage
To use useObserver
, call it within your component body with a function that accesses reactive state:
Basic Usage
import { useAnchor } from '@anchorlib/react';
import { useObserver } from '@anchorlib/react';
const UserProfile = () => {
const [user] = useAnchor({
firstName: 'John',
lastName: 'Doe',
age: 30,
});
// useObserver tracks user.firstName and user.lastName
const fullName = useObserver(() => {
console.log('Recalculating full name...');
return `${user.firstName} ${user.lastName}`;
});
const incrementAge = () => {
user.age++; // This will NOT trigger fullName recalculation
};
const changeName = () => {
user.firstName = 'Jane';
user.lastName = 'Smith'; // This WILL trigger fullName recalculation and re-render
};
return (
<div>
<h1>Welcome, {fullName}!</h1>
<p>Age: {user.age}</p>
<button onClick={incrementAge}>Increment Age</button>
<button onClick={changeName}>Change Name</button>
</div>
);
};
With Additional Dependencies
import { useAnchor, useVariable } from '@anchorlib/react';
import { useObserver } from '@anchorlib/react';
const ProductCard = ({ currency }) => {
const [product] = useAnchor({
name: 'Laptop',
price: 1000,
discount: 0.1,
});
// useObserver tracks product.price and product.discount
// It also re-runs when the currency prop changes
const displayPrice = useObserver(() => {
const discountedPrice = product.price * (1 - product.discount);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(discountedPrice);
}, [currency]);
const updatePrice = () => {
product.price = 1200; // This will trigger displayPrice recalculation
};
const updateDiscount = () => {
product.discount = 0.15; // This will also trigger displayPrice recalculation
};
return (
<div>
<h2>{product.name}</h2>
<p>Price: {displayPrice}</p>
<button onClick={updatePrice}>Update Price</button>
<button onClick={updateDiscount}>Update Discount</button>
</div>
);
};
When to use it?
Use useObserver
when you need to create a reactive computation that derives values from reactive state. It's the most common hook for creating computed values that automatically update when their dependencies change.
Note that unlike observe(), useObserver
triggers re-render of the entire component where it's declared, not just the observed part.
useObserved(observe, deps?)
(Deprecated)
Deprecated
This API is deprecated. Use useObserver instead.
useObservedList(state, key?)
Derives a list of objects from a reactive array state, providing stable keys for rendering. This is particularly useful when rendering lists in React where you need stable keys for efficient reconciliation.
Params
state
- The reactive array state.key
(optional) - A property name to use as the key for each item. If not provided, the array index will be used.
Usage
Using Index as Key
import { useAnchor } from '@anchorlib/react';
import { useObservedList } from '@anchorlib/react';
const TodoList = () => {
const [todos] = useAnchor([
{ id: 1, text: 'Learn Anchor', completed: false },
{ id: 2, text: 'Build an app', completed: false },
{ id: 3, text: 'Deploy to production', completed: false },
]);
// Using index as key
const observedTodos = useObservedList(todos);
const toggleTodo = (index) => {
todos[index].completed = !todos[index].completed;
};
return (
<ul>
{observedTodos.map(({ key, value: todo }, index) => (
<li key={key}>
<input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(index)} />
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.text}</span>
</li>
))}
</ul>
);
};
Using Custom Property as Key
import { useAnchor } from '@anchorlib/react';
import { useObservedList } from '@anchorlib/react';
const UserList = () => {
const [users] = useAnchor([
{ id: 'user1', name: 'John Doe', email: 'john@example.com' },
{ id: 'user2', name: 'Jane Smith', email: 'jane@example.com' },
{ id: 'user3', name: 'Bob Johnson', email: 'bob@example.com' },
]);
// Using 'id' property as key
const observedUsers = useObservedList(users, 'id');
const updateUser = (userId, newName) => {
const user = users.find((u) => u.id === userId);
if (user) {
user.name = newName;
}
};
return (
<ul>
{observedUsers.map(({ key, value: user }) => (
<li key={key}>
<span>
{user.name} ({user.email})
</span>
<button onClick={() => updateUser(user.id, 'Updated Name')}>Update Name</button>
</li>
))}
</ul>
);
};
When to use it?
Use useObservedList
when you need to render a list of reactive objects and want to ensure stable keys for efficient React reconciliation. This hook is especially useful when working with reactive arrays where items can be added, removed, or reordered.
Low Level APIs
These are lower-level APIs primarily intended for internal use or advanced scenarios.
useObserverRef(deps?, displayName?)
Provides a stable StateObserver
instance for tracking reactive dependencies. This is a low-level hook.
Params
deps
(optional) - Dependencies that, when changed, re-establish the observer.displayName
(optional) - Name for debugging.
Usage
Basic Usage
import { useAnchor } from '@anchorlib/react';
import { useObserverRef } from '@anchorlib/react';
const CustomObserver = () => {
const [data] = useAnchor({ value: 0, text: 'Hello' });
// Get an observer instance
const [observer, version] = useObserverRef([], 'CustomObserver');
// Use the observer to track reactive dependencies
const observedValue = React.useMemo(() => {
return observer.run(() => {
// This access will be tracked by the observer
return `Value: ${data.value}, Text: ${data.text}`;
});
}, [observer, version, data]);
const updateValue = () => {
data.value++;
};
return (
<div>
<p>{observedValue}</p>
<button onClick={updateValue}>Update Value</button>
</div>
);
};
When to use it?
Use useObserverRef
when you need direct access to an observer instance for advanced scenarios. Most developers will not need to use this hook directly, as higher-level APIs like useObserver provide easier ways to work with reactive computations.