Commiq Docs
Usage Patterns

Async Loading States

A standard pattern for async commands with loading, success, and error state handling.

These are the v1 docs. They are not maintained.

This page is a verbatim snapshot of the commiq 1.x documentation, kept for people still running 1.x. It is not corrected and not tested against any release. Several pages contain claims that were wrong when written: examples that do not run as printed, sealStore described as read-only when it was not, handledEvent presented as a working subscription pattern when it never matched, and effects described as swallowing only AbortError when they swallowed everything.

Do not treat anything here as a statement about how commiq 2.x behaves.

What changed in 2.0 and how to upgrade · Current documentation

Site search only indexes the current docs. Use the sidebar to browse these pages.

Async Loading States

Async command handlers naturally map to three phases: loading, success, and error. Representing these consistently in the state shape makes components predictable and eliminates edge cases.

State shape

Use a status discriminant rather than separate boolean flags:

type UserState = {
  id: string | null;
  name: string | null;
  status: "idle" | "loading" | "error";
  errorMessage: string | null;
};

export const initialState: UserState = {
  id: null,
  name: null,
  status: "idle",
  errorMessage: null,
};

A status enum cannot represent impossible combinations. Two boolean flags (isLoading, isError) can both be true at the same time — status cannot.

Command handler

import { createStore, createEvent, sealStore } from "@naikidev/commiq";

type User = { id: string; name: string };

export const UserEvent = {
  Fetched: createEvent<User>("user:fetched"),
  FetchFailed: createEvent<{ message: string }>("user:fetch-failed"),
};

const _store = createStore<UserState>(initialState);

_store.addCommandHandler<{ id: string }>("user:fetch", async (ctx, cmd) => {
  ctx.setState({ ...ctx.state, status: "loading", errorMessage: null });

  try {
    const user = await fetchUserById(cmd.data.id);
    ctx.setState({ id: user.id, name: user.name, status: "idle", errorMessage: null });
    ctx.emit(UserEvent.Fetched, user);
  } catch (e) {
    ctx.setState({ ...ctx.state, status: "error", errorMessage: (e as Error).message });
    ctx.emit(UserEvent.FetchFailed, { message: (e as Error).message });
  }
});

export const userStore = sealStore(_store);

The handler transitions through three states in sequence:

  1. Set status: "loading" synchronously as the first line — this is immediate feedback.
  2. On success: update the data fields and set status: "idle". Emit a success event.
  3. On failure: record the error message and set status: "error". Emit a failure event.

Emitting events on both outcomes allows other stores and UI layers to react without polling state.

Selectors

Write targeted selectors to avoid unnecessary re-renders:

function UserProfile({ userId }: { userId: string }) {
  const status = useSelector(userStore, (s) => s.status);
  const name = useSelector(userStore, (s) => s.name);
  const errorMessage = useSelector(userStore, (s) => s.errorMessage);
  const queue = useQueue(userStore);

  useEffect(() => {
    queue(UserCommand.fetchUser(userId));
  }, [userId]);

  if (status === "loading") return <Spinner />;
  if (status === "error") return <ErrorMessage message={errorMessage} />;
  return <p>{name}</p>;
}

Each useSelector call subscribes independently. A component that only reads status will not re-render when name changes. For encapsulating this into a reusable hook, see domain hooks.

Reset

Provide an explicit reset command to clear state when navigating away or when the data is no longer relevant:

_store.addCommandHandler("user:reset", (ctx) => {
  ctx.setState(initialState);
});

Exporting initialState alongside the store ensures the reset is consistent with the initial state definition. Avoid duplicating the initial values in the reset handler.

Multiple async operations

When a store manages several independent async operations, track each one with its own status field:

type DashboardState = {
  user: { data: User | null; status: "idle" | "loading" | "error" };
  orders: { data: Order[]; status: "idle" | "loading" | "error" };
};

This allows components to show granular loading states — a spinner next to the orders table while the user header has already loaded.

Do not queue a second fetch command while a fetch is already in progress unless you intend to cancel the first. Commiq processes commands sequentially, so a second user:fetch queued while the first is still running will wait and then execute after the first completes.

On this page