Guards
Guards are pure synchronous predicates that determine whether a transition should occur. They receive the current context and the triggering event, and return true to allow the transition or false to block it.
Basic Guard
Inline guards are the simplest form:
import { createMachine } from "effstate";
const machine = createMachine({ id: "form", initial: "editing", context: ContextSchema, initialContext: { isValid: false, data: {} }, states: { editing: { on: { SUBMIT: { target: "submitting", guard: ({ context }) => context.isValid, }, }, }, submitting: { /* ... */ }, },});Named Guards with guard()
Use the guard() helper to create reusable guards:
import { guard } from "effstate";
const isValid = guard<MyContext, MyEvent>( ({ context }) => context.isValid);
const hasMinItems = guard<MyContext, MyEvent>( ({ context }) => context.items.length >= 3);
const machine = createMachine({ states: { editing: { on: { SUBMIT: { target: "submitting", guard: isValid, }, ADD_ITEM: { target: "ready", guard: hasMinItems, }, }, }, },});Guard Combinators
Compose guards using and, or, and not:
import { and, or, not, guard } from "effstate";
const isAdmin = guard<MyContext, MyEvent>( ({ context }) => context.user.role === "admin");
const isOwner = guard<MyContext, MyEvent>( ({ context }) => context.user.id === context.resource.ownerId);
const isPublished = guard<MyContext, MyEvent>( ({ context }) => context.resource.status === "published");
// Complex permission: admin OR (owner AND not published)const canEdit = or( isAdmin, and(isOwner, not(isPublished)));
const machine = createMachine({ states: { viewing: { on: { EDIT: { target: "editing", guard: canEdit, }, }, }, },});Event-Based Guards
Guards can also check event data:
import { Data } from "effect";
class Transfer extends Data.TaggedClass("TRANSFER")<{ amount: number; toAccount: string;}> {}
const machine = createMachine({ states: { ready: { on: { TRANSFER: { target: "transferring", guard: ({ context, event }) => event.amount > 0 && event.amount <= context.balance && event.toAccount !== context.accountId, }, }, }, },});Multiple Transitions with Guards
Define multiple transitions for the same event with different guards:
states: { processing: { on: { COMPLETE: [ { target: "success", guard: ({ context }) => context.errors.length === 0, }, { target: "partialSuccess", guard: ({ context }) => context.errors.length < 3, }, { // No guard = default/fallback target: "failed", }, ], }, },}The first transition whose guard returns true (or has no guard) will be taken.
Guards on Invoke Handlers
Guards can also control invoke result handling:
invoke: invoke({ src: () => fetchUser(), onSuccess: [ { target: "admin", guard: ({ event }) => event.output.role === "admin", }, { target: "regular", // No guard - default case }, ],})Type-Safe Guards
For full type safety, explicitly type your guards:
import { guard } from "effstate";import type { Guard } from "effstate";
// Using guard() helperconst isReady = guard<MyContext, MyEvent>( ({ context }) => context.status === "ready");
// Or inline with explicit typesconst canProceed: Guard<MyContext, MyEvent> = ({ context, event }) => context.isReady && event.confirmed;Async Validation Pattern
Guards are synchronous. For async validation, use a state machine pattern:
states: { idle: { on: { SUBMIT: { target: "validating" }, }, }, validating: { invoke: invoke({ src: ({ context }) => validateData(context.data), onSuccess: { target: "valid", guard: ({ event }) => event.output.isValid, }, onSuccess: { target: "invalid", // Fallback when validation fails }, onFailure: { target: "error" }, }), }, valid: { /* proceed */ }, invalid: { /* show errors */ },}Common Guard Patterns
Feature Flags
const featureEnabled = guard<MyContext, MyEvent>( ({ context }) => context.features.includes("newFeature"));User Roles
const isAuthorized = guard<MyContext, MyEvent>( ({ context }) => ["admin", "moderator"].includes(context.user.role));Rate Limiting
const notRateLimited = guard<MyContext, MyEvent>( ({ context }) => { const now = Date.now(); const elapsed = now - context.lastAction; return elapsed > 1000; // 1 second cooldown });Validation
const isFormValid = guard<FormContext, SubmitEvent>( ({ context }) => context.email.includes("@") && context.password.length >= 8 && context.acceptedTerms);