Optimistic UI
Optimistic UI is a user interface pattern for immediate feedback. It assumes an action will succeed and updates the UI instantly, rolling back to the previous state only if the underlying operation (like a network request) fails.
The AIR Stack provides several ways to implement this pattern depending on your needs.
The undoable Primitive
The simplest approach uses the undoable() primitive. It applies mutations instantly and provides an undo function to rollback if the network request fails, along with a settled callback to finalize the state.
import { setup, render, undoable } from '@anchorlib/react';
export const LikeButton = setup<{ post: any }>((props) => {
const toggleLike = async () => {
// Instantly apply the mutation and get rollback functions
const [undo, settled] = undoable(() => {
props.post.liked = !props.post.liked;
});
// Attempt the remote operation
await likePost(props.post.id)
.then(settled) // Finalize if successful
.catch((e) => {
undo(); // Rollback if failed
console.error(e);
});
};
return render(() => (
<button onClick={toggleLike}>
{props.post.liked ? 'Unlike' : 'Like'}
</button>
));
});Custom State Tracking
For complex scenarios requiring sequential state mutations separated by asynchronous boundaries (await), where the rollback itself requires manual orchestration, you can track the previous state explicitly.
import { setup, render } from '@anchorlib/react';
export const Checkout = setup<{ cart: any }>((props) => {
const processCheckout = async () => {
// 1. Manually track the state
let prevStatus = props.cart.status;
try {
// 2. First mutation
props.cart.status = 'locking';
await api.lockInventory(props.cart.id);
// Update tracking variable before next step
prevStatus = props.cart.status;
// 3. Second mutation
props.cart.status = 'paying';
await api.processPayment(props.cart.id);
props.cart.status = 'complete';
} catch (e) {
// 4. Automatically restores the immediately previous state, regardless of where it failed
props.cart.status = prevStatus;
}
};
return render(() => (
<button onClick={processCheckout}>Checkout</button>
));
});Workflows
For complex, multi-stage pipelines, you can use the workflow engine where each step handles its own isolated optimistic updates and rollback logic. This prevents massive centralized error handlers and keeps logic decoupled.
import { setup, render, plan, undoable, mutable } from '@anchorlib/react';
export const Checkout = setup(() => {
const cart = mutable({ id: 'cart_123', status: 'idle' });
// Define the workflow natively bound to the component's state
const checkoutFlow = plan()
.then(async () => {
const [undo, settled] = undoable(() => cart.status = 'locking');
await api.lockInventory(cart.id).then(settled).catch((e) => { undo(); throw e; });
})
.then(async () => {
const [undo, settled] = undoable(() => cart.status = 'paying');
await api.processPayment(cart.id).then(settled).catch((e) => { undo(); throw e; });
cart.status = 'complete';
});
return render(() => (
<button onClick={checkoutFlow}>
{cart.status === 'idle' ? 'Checkout' : cart.status}
</button>
));
});