Skip to content

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.

tsx
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.

tsx
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.

tsx
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>
  ));
});