Commiq Docs
Usage Patterns

Event-Driven Side Effects

Trigger notifications, navigation, and analytics from store events without coupling logic to views.

Event-Driven Side Effects

Not all reactions to store events should be state changes. Showing a toast notification, navigating after a successful login, or logging an analytics event are concerns of the UI layer — they should not live in the store.

The useEvent hook subscribes a component to store events and runs a callback when they fire, without adding transient flags to the state.

The problem with state flags

The alternative — adding a justSaved or loginRedirectPending flag to state — creates noise and requires cleanup:

// Avoid this approach
type FormState = {
  saved: boolean;
  justSaved: boolean; // Only exists to trigger a toast — belongs in UI, not store
};

Managing flag resets adds commands, adds complexity, and still does not prevent a component from missing the transition if it renders after the flag was set.

The pattern

Emit a semantic event from the command handler:

stores/form.ts
export const FormEvent = {
  Saved: createEvent<{ id: string }>("form:saved"),
  SaveFailed: createEvent<{ message: string }>("form:save-failed"),
};

_store.addCommandHandler("form:save", async (ctx, cmd) => {
  try {
    const result = await saveFormData(cmd.data);
    ctx.setState({ ...ctx.state, dirty: false });
    ctx.emit(FormEvent.Saved, { id: result.id });
  } catch (e) {
    ctx.emit(FormEvent.SaveFailed, { message: (e as Error).message });
  }
});

Subscribe to the event in the component that owns the side effect:

import { useEvent } from "@naikidev/commiq-react";
import { formStore, FormEvent } from "../stores/form";

function FormPage() {
  useEvent(formStore, FormEvent.Saved, (event) => {
    showToast(`Saved (ID: ${event.data.id})`);
  });

  useEvent(formStore, FormEvent.SaveFailed, (event) => {
    showToast(`Save failed: ${event.data.message}`, { type: "error" });
  });

  return <FormEditor />;
}

The store emits what happened. The component decides what to do about it. Neither knows about the other.

import { useNavigate } from "react-router-dom";
import { useEvent } from "@naikidev/commiq-react";
import { authStore, AuthEvent } from "../stores/auth";

function LoginPage() {
  const navigate = useNavigate();

  useEvent(authStore, AuthEvent.SignedIn, () => {
    navigate("/dashboard");
  });

  return <LoginForm />;
}

Routing libraries are a UI concern. The auth store emits AuthEvent.SignedIn and is not aware of React Router.

Analytics

Centralizing analytics in a shell or layout component keeps tracking code out of store logic and out of individual feature components:

function AppShell() {
  useEvent(orderStore, OrderEvent.Placed, (event) => {
    analytics.track("order_placed", {
      orderId: event.data.id,
      total: event.data.total,
    });
  });

  useEvent(authStore, AuthEvent.SignedOut, () => {
    analytics.track("signed_out");
  });

  return <Outlet />;
}

All useEvent subscriptions clean up automatically when the component unmounts, so analytics tracking stops when the shell is removed.

State vs. events

ReactionBelongs in stateBelongs in useEvent
Loading spinnerYesNo
Inline error messageYesNo
Transient toast notificationNoYes
Navigate to another pageNoYes
Analytics eventNoYes
Disabled button after submissionYesNo

The distinction: state represents what is currently true about the domain. Events represent what just happened. Side effects triggered by "what happened" belong in useEvent, not in state.

Placing useEvent calls

Use useEvent in the component that owns the side effect — typically a page component, a layout, or a notification provider. Avoid placing it deep in a component tree where the subscription lifetime is tied to a rarely-rendered component.

// Good: the page owns the navigation side effect
function CheckoutPage() {
  useEvent(checkoutStore, CheckoutEvent.Completed, () => {
    navigate("/order-confirmation");
  });
  return <CheckoutForm />;
}

// Avoid: a leaf component triggering navigation is hard to trace
function SubmitButton() {
  useEvent(checkoutStore, CheckoutEvent.Completed, () => {
    navigate("/order-confirmation"); // Unexpected location for a navigation side effect
  });
  return <button>Place Order</button>;
}

On this page