Commiq Docs
Usage Patterns

Optimistic Updates

Update state immediately for responsiveness and reconcile when the async operation completes.

Optimistic Updates

Users expect immediate feedback. An optimistic update applies the expected state change synchronously — before the server confirms — and corrects it if the operation fails. Commiq's command handler structure makes this natural: call setState at the top of the handler, then reconcile after the async work finishes.

The pattern

Apply the optimistic state

Capture the current state for rollback, then set the expected outcome immediately.

Run the async operation

Call the API, write to the database, or perform any async work.

Reconcile

On success, confirm with server-returned data if needed. On failure, restore the captured state and emit an error event.

Optimistic list add

Add an item to the list immediately. If the API call fails, remove it:

stores/todo.ts
const TodoEvent = {
  AddFailed: createEvent<{ tempId: string; message: string }>("todo:add-failed"),
};

_store.addCommandHandler<{ text: string }>("todo:add", async (ctx, cmd) => {
  const tempId = `temp-${Date.now()}`;

  // ── Optimistic: add immediately with a pending flag ──
  ctx.setState({
    items: [...ctx.state.items, { id: tempId, text: cmd.data.text, pending: true }],
  });

  try {
    const created = await createTodoOnServer(cmd.data.text);

    // ── Confirm: replace the temp item with the real one ──
    ctx.setState({
      items: ctx.state.items.map((item) =>
        item.id === tempId ? { id: created.id, text: created.text, pending: false } : item,
      ),
    });
  } catch (e) {
    // ── Rollback: remove the temp item ──
    ctx.setState({
      items: ctx.state.items.filter((item) => item.id !== tempId),
    });
    ctx.emit(TodoEvent.AddFailed, { tempId, message: (e as Error).message });
  }
});

The pending flag lets the UI render optimistic items differently — a reduced opacity or a spinner next to the item — so users understand the item is not yet confirmed.

components/TodoItem.tsx
function TodoItem({ item }: { item: Todo }) {
  return (
    <li style={{ opacity: item.pending ? 0.6 : 1 }}>
      {item.text}
      {item.pending && <span> (saving...)</span>}
    </li>
  );
}

Optimistic toggle

Toggle a boolean immediately and revert on failure:

stores/bookmark.ts
export const BookmarkEvent = {
  ToggleFailed: createEvent<{ articleId: string }>("bookmark:toggle-failed"),
};

_store.addCommandHandler<{ articleId: string }>("bookmark:toggle", async (ctx, cmd) => {
  const { articleId } = cmd.data;
  const prev = ctx.state.bookmarked;

  // ── Optimistic: flip immediately ──
  ctx.setState({ ...ctx.state, bookmarked: !prev });

  try {
    if (!prev) {
      await addBookmark(articleId);
    } else {
      await removeBookmark(articleId);
    }
  } catch (e) {
    // ── Rollback: restore previous value ──
    ctx.setState({ ...ctx.state, bookmarked: prev });
    ctx.emit(BookmarkEvent.ToggleFailed, { articleId });
  }
});

Surface the failure as a transient notification using event-driven side effects:

useEvent(bookmarkStore, BookmarkEvent.ToggleFailed, () => {
  showToast("Could not update bookmark. Try again.", { type: "error" });
});

Rollback with captured snapshots

For more complex state, capture a snapshot of the affected portion before the optimistic write:

stores/settings.ts
_store.addCommandHandler<Partial<Settings>>("settings:update", async (ctx, cmd) => {
  const snapshot = { ...ctx.state.settings };

  // ── Optimistic ──
  ctx.setState({
    ...ctx.state,
    settings: { ...ctx.state.settings, ...cmd.data },
  });

  try {
    await saveSettings(cmd.data);
  } catch (e) {
    // ── Rollback to snapshot ──
    ctx.setState({ ...ctx.state, settings: snapshot });
    ctx.emit(SettingsEvent.UpdateFailed, { message: (e as Error).message });
  }
});

Capture the snapshot before calling setState. After setState, ctx.state reflects the new value — the original is lost unless you save it first.

When not to use optimistic updates

Optimistic updates are not always appropriate:

ScenarioWhy to avoid
The operation generates server-side data the UI needs (IDs, timestamps, computed values)The optimistic state would be incomplete — components expecting the real ID would break
The operation has visible side effects beyond state (sending an email, charging a card)Rolling back state does not undo the real-world action
The failure rate is highUsers see constant flicker between optimistic and rolled-back states
Multiple users edit the same resource concurrentlyThe optimistic state may conflict with another user's update

In these cases, use a standard async loading state — show a loading indicator, wait for confirmation, then update.

On this page