{"slug":"building-machines","title":"Building Machines","description":"Deep dive into the key concepts that power Zag machines","contentType":"guides","content":"This guide provides a deep dive into the key pieces that make Zag machines\nframework-agnostic and reactive.\n\n## Context\n\nContext holds the reactive state of your machine. Unlike props (which are\nconfiguration), context represents internal state that changes over time.\n\nContext values are created using the `bindable` pattern, which provides\ncontrolled/uncontrolled state management.\n\n```tsx\ncontext({ bindable, prop }) {\n  return {\n    // Uncontrolled state with default value\n    count: bindable<number>(() => ({\n      defaultValue: 0,\n    })),\n\n    // Controlled/uncontrolled state\n    name: bindable<string>(() => ({\n      defaultValue: prop(\"defaultName\") ?? \"\",\n      value: prop(\"name\"), // When provided, state is controlled\n      onChange(value, prev) {\n        prop(\"onNameChange\")?.({ name: value })\n      },\n    })),\n  }\n}\n```\n\n### Bindable Parameters\n\n- **`defaultValue`**: Initial value for uncontrolled state\n- **`value`**: Controlled value from props (when provided, state becomes\n  controlled)\n- **`onChange`**: Callback fired when value changes\n- **`isEqual`**: Custom equality function (defaults to `Object.is`)\n- **`hash`**: Custom hash function for change detection\n- **`sync`**: Whether to use synchronous updates (framework-specific)\n\n### Accessing Context\n\nIn actions, guards, computed, and effects:\n\n```tsx\nactions: {\n  increment({ context }) {\n    const current = context.get(\"count\")\n    context.set(\"count\", current + 1) // Update context\n  },\n\n  reset({ context }) {\n    const initial = context.initial(\"count\") // Get initial value\n    context.set(\"count\", initial)\n  },\n\n  logName({ context }) {\n    const name = context.get(\"name\") // Read current value\n    console.log(\"Current name:\", name)\n  }\n}\n```\n\n### How Bindable Works\n\nThe `bindable` pattern automatically:\n\n- Detects controlled vs uncontrolled state (`value !== undefined`)\n- Manages framework-specific reactivity (React hooks, Solid signals, Vue refs,\n  Svelte runes)\n- Handles change notifications with equality checks\n- Provides a consistent API across all frameworks\n\n## Watch\n\nThe `watch` function allows you to reactively respond to changes in props or\ncontext. It uses the `track` function to specify dependencies and trigger\nactions when they change.\n\n```tsx\nwatch({ track, action, prop, context }) {\n  // Track prop changes\n  track([() => prop(\"enabled\")], () => {\n    action([\"updateEnabledState\"])\n  })\n\n  // Track context changes\n  track([() => context.get(\"count\")], () => {\n    action([\"logCount\", \"updateDisplay\"])\n  })\n\n  // Track multiple dependencies\n  track([\n    () => context.get(\"count\"),\n    () => prop(\"multiplier\")\n  ], () => {\n    action([\"calculateTotal\"])\n  })\n}\n```\n\n### How Track Works\n\n1. **Dependency Array**: Array of functions that return values to track\n2. **Effect Function**: Runs when any tracked dependency changes\n3. **Change Detection**: Uses deep equality comparison (or custom `isEqual` from\n   bindable)\n4. **Framework Integration**: Each framework implements track using its\n   reactivity:\n   - React: `useEffect` with dependency tracking\n   - Solid: `createEffect` with reactive signals\n   - Vue: `watch` with computed dependencies\n   - Svelte: Reactive statements\n\n### Common Patterns\n\n```tsx\n// Sync controlled props\nwatch({ track, action, prop }) {\n  track([() => prop(\"enabled\")], () => {\n    action([\"syncEnabledState\"])\n  })\n}\n\n// React to context changes\nwatch({ track, action, context }) {\n  track([() => context.get(\"count\")], () => {\n    action([\"notifyCountChanged\"])\n  })\n}\n\n// Track multiple values\nwatch({ track, action, context, prop }) {\n  track([\n    () => context.get(\"firstName\"),\n    () => context.get(\"lastName\")\n  ], () => {\n    action([\"updateFullName\"])\n  })\n}\n```\n\n## Computed\n\nComputed values are derived from context, props, refs, and other computed\nvalues. They're recalculated whenever their dependencies change and are memoized\nper framework.\n\n```tsx\ncomputed: {\n  isEven({ context }) {\n    return context.get(\"count\") % 2 === 0\n  },\n\n  fullName({ context }) {\n    const first = context.get(\"firstName\") || \"\"\n    const last = context.get(\"lastName\") || \"\"\n    return `${first} ${last}`.trim()\n  },\n\n  // Computed can depend on other computed values\n  status({ computed, context }) {\n    const isEven = computed(\"isEven\")\n    const count = context.get(\"count\")\n    return isEven ? `Even: ${count}` : `Odd: ${count}`\n  },\n}\n```\n\n### Accessing Computed\n\n```tsx\n// In guards\nguards: {\n  canIncrement({ computed }) {\n    return computed(\"isEven\") // Only increment when count is even\n  }\n}\n\n// In actions\nactions: {\n  logStatus({ computed }) {\n    const status = computed(\"status\")\n    console.log(\"Status:\", status)\n  }\n}\n\n// In other computed values\ncomputed: {\n  message({ computed }) {\n    return computed(\"isEven\") ? \"Count is even\" : \"Count is odd\"\n  }\n}\n```\n\n### Key Points\n\n- Computed values are **lazy** - only calculated when accessed\n- They can depend on props, context, refs, scope, and other computed values\n- They're **memoized** per framework (React `useMemo`, Solid `createMemo`, etc.)\n- Use computed for values that derive from state but don't need to be stored in\n  context\n\n## Refs\n\nRefs hold non-reactive references like class instances, DOM elements, or other\nobjects that don't need reactivity. Unlike context, refs don't trigger\nre-renders when changed.\n\n```tsx\nrefs() {\n  return {\n    // Simple counter for internal tracking\n    operationCount: 0,\n\n    // Cache for storing previous values\n    previousCount: null,\n\n    // Simple object for tracking state\n    history: [],\n  }\n}\n```\n\n### Accessing Refs\n\n```tsx\nactions: {\n  increment({ refs, context }) {\n    const count = context.get(\"count\")\n\n    // Store previous value in ref\n    refs.set(\"previousCount\", count)\n\n    // Track operation\n    const ops = refs.get(\"operationCount\")\n    refs.set(\"operationCount\", ops + 1)\n\n    // Update context\n    context.set(\"count\", count + 1)\n  },\n\n  saveHistory({ refs, context }) {\n    const history = refs.get(\"history\")\n    const count = context.get(\"count\")\n    history.push(count)\n    refs.set(\"history\", history)\n  }\n}\n```\n\n### When to Use Refs\n\n- Class instances that manage their own state\n- Cached values that don't need reactivity\n- Temporary state that doesn't affect rendering\n- Performance-critical data that shouldn't trigger updates\n\n## Props\n\nProps are the configuration passed to your machine. The `props` function\nnormalizes and sets defaults.\n\n```tsx\nprops({ props, scope }) {\n  return {\n    // Set defaults\n    step: 1,\n    min: 0,\n    max: 100,\n\n    // Conditional defaults\n    enabled: props.disabled === undefined ? true : !props.disabled,\n\n    // Normalize values\n    initialValue: props.initialValue ?? 0,\n\n    // Merge nested objects\n    settings: {\n      showLabel: true,\n      showButtons: true,\n      ...props.settings,\n    },\n\n    // User props override defaults (spread last)\n    ...props,\n  }\n}\n```\n\n### Key Principles\n\n- Always return defaults **first**, then spread `...props` to allow overrides\n- Use conditional logic for interdependent defaults\n- The `scope` parameter provides access to DOM scope (id, ids, getRootNode)\n\n### Accessing Props\n\n```tsx\n// In guards\nguards: {\n  canIncrement({ prop, context }) {\n    const max = prop(\"max\")\n    const count = context.get(\"count\")\n    return count < max\n  }\n}\n\n// In actions\nactions: {\n  notifyChange({ prop, context }) {\n    const count = context.get(\"count\")\n    prop(\"onChange\")?.({ count })\n  }\n}\n\n// In computed\ncomputed: {\n  canDecrement({ prop, context }) {\n    const min = prop(\"min\")\n    const count = context.get(\"count\")\n    return count > min\n  }\n}\n```\n\n## Scope\n\nScope provides access to DOM-related utilities and element queries. It's\navailable in props, actions, guards, computed, and effects. Scope helps machines\ninteract with the DOM in a framework-agnostic way.\n\n```tsx\n// Scope interface\ninterface Scope {\n  id?: string // Unique machine instance ID\n  ids?: Record<string, any> // Map of part IDs\n  getRootNode: () => ShadowRoot | Document | Node\n  getById: <T extends Element = HTMLElement>(id: string) => T | null\n  getActiveElement: () => HTMLElement | null\n  isActiveElement: (elem: HTMLElement | null) => boolean\n  getDoc: () => typeof document\n  getWin: () => typeof window\n}\n```\n\n### Using Scope\n\nScope is commonly used in effects and actions to interact with DOM elements:\n\n```tsx\neffects: {\n  focusInput({ scope }) {\n    const inputEl = scope.getById(\"input\")\n    inputEl?.focus()\n  },\n\n  trackClickOutside({ scope, send }) {\n    const doc = scope.getDoc()\n\n    function handleClick(event: MouseEvent) {\n      const rootEl = scope.getRootNode()\n      if (!rootEl.contains(event.target as Node)) {\n        send({ type: \"CLICK_OUTSIDE\" })\n      }\n    }\n\n    doc.addEventListener(\"click\", handleClick)\n    return () => {\n      doc.removeEventListener(\"click\", handleClick)\n    }\n  }\n}\n```\n\n### Common Patterns\n\n```tsx\n// Get element by ID\nactions: {\n  scrollToElement({ scope }) {\n    const element = scope.getById(\"target\")\n    element?.scrollIntoView()\n  }\n}\n\n// Check active element\nguards: {\n  isInputFocused({ scope }) {\n    const inputEl = scope.getById(\"input\")\n    return scope.isActiveElement(inputEl)\n  }\n}\n\n// Access document/window\neffects: {\n  preventScroll({ scope }) {\n    const doc = scope.getDoc()\n    const originalOverflow = doc.body.style.overflow\n    doc.body.style.overflow = \"hidden\"\n\n    return () => {\n      doc.body.style.overflow = originalOverflow\n    }\n  }\n}\n\n// Use in props for conditional defaults\nprops({ props, scope }) {\n  return {\n    // Use scope.id to generate unique IDs\n    id: props.id ?? scope.id ?? `counter-${Math.random()}`,\n    ...props,\n  }\n}\n```\n\n### Key Points\n\n- Scope provides framework-agnostic DOM access\n- Use `getById` to query elements by their generated IDs\n- Use `getRootNode` to get the root container (supports Shadow DOM)\n- Use `getDoc` and `getWin` for document/window access\n- Scope is typically used in effects for DOM manipulation and event listeners\n\n## Actions, Guards, and Effects\n\nThese are the implementation details that bring your machine to life.\n\n### Actions\n\nActions perform state updates and side effects:\n\n```tsx\nactions: {\n  increment({ context, prop }) {\n    const step = prop(\"step\")\n    const current = context.get(\"count\")\n    context.set(\"count\", current + step)\n  },\n\n  notifyChange({ prop, context }) {\n    const count = context.get(\"count\")\n    prop(\"onChange\")?.({ count })\n  },\n\n  reset({ context }) {\n    const initial = context.initial(\"count\")\n    context.set(\"count\", initial)\n  }\n}\n```\n\n### Guards\n\nGuards are boolean conditions that determine if a transition should occur:\n\n```tsx\nguards: {\n  canIncrement({ prop, context }) {\n    const max = prop(\"max\")\n    const count = context.get(\"count\")\n    return count < max\n  },\n\n  canDecrement({ prop, context }) {\n    const min = prop(\"min\")\n    const count = context.get(\"count\")\n    return count > min\n  }\n}\n```\n\n### Effects\n\nEffects are side effects that run while in a state and must return cleanup:\n\n```tsx\neffects: {\n  logCount({ context, send }) {\n    const count = context.get(\"count\")\n    console.log(\"Count changed to:\", count)\n\n    // No cleanup needed for this effect\n    return undefined\n  },\n\n  startTimer({ send }) {\n    const intervalId = setInterval(() => {\n      send({ type: \"TICK\" })\n    }, 1000)\n\n    // Return cleanup function\n    return () => {\n      clearInterval(intervalId)\n    }\n  }\n}\n```\n\n**Important**: Effects must return a cleanup function (or `undefined` if no\ncleanup needed). Cleanup is called when exiting the state or unmounting.\n\n## Putting It All Together\n\nHere's a complete example showing how these concepts work together in a simple\ncounter machine:\n\n```tsx\nexport const machine = createMachine<CounterSchema>({\n  // 1. Normalize props\n  props({ props }) {\n    return {\n      step: 1,\n      min: 0,\n      max: 100,\n      defaultValue: 0,\n      ...props,\n    }\n  },\n\n  // 2. Define reactive context\n  context({ prop, bindable }) {\n    return {\n      count: bindable(() => ({\n        defaultValue: prop(\"defaultValue\"),\n        value: prop(\"value\"),\n        onChange(value) {\n          prop(\"onChange\")?.({ count: value })\n        },\n      })),\n    }\n  },\n\n  // 3. Define non-reactive refs\n  refs() {\n    return {\n      previousCount: null,\n      operationCount: 0,\n    }\n  },\n\n  // 4. Define computed values\n  computed: {\n    isEven({ context }) {\n      return context.get(\"count\") % 2 === 0\n    },\n    canIncrement({ prop, context }) {\n      const max = prop(\"max\")\n      const count = context.get(\"count\")\n      return count < max\n    },\n    canDecrement({ prop, context }) {\n      const min = prop(\"min\")\n      const count = context.get(\"count\")\n      return count > min\n    },\n  },\n\n  // 5. Watch for changes\n  watch({ track, action, context, prop }) {\n    track([() => context.get(\"count\")], () => {\n      action([\"logCount\", \"notifyChange\"])\n    })\n    track([() => prop(\"step\")], () => {\n      action([\"logStepChanged\"])\n    })\n  },\n\n  // 6. Define states and transitions\n  states: {\n    idle: {\n      on: {\n        INCREMENT: {\n          guard: \"canIncrement\",\n          actions: [\"increment\"],\n        },\n        DECREMENT: {\n          guard: \"canDecrement\",\n          actions: [\"decrement\"],\n        },\n        RESET: {\n          actions: [\"reset\"],\n        },\n      },\n    },\n  },\n\n  // 7. Implement actions, guards, effects\n  implementations: {\n    guards: {\n      canIncrement({ computed }) {\n        return computed(\"canIncrement\")\n      },\n      canDecrement({ computed }) {\n        return computed(\"canDecrement\")\n      },\n    },\n    actions: {\n      increment({ context, prop, refs }) {\n        const step = prop(\"step\")\n        const current = context.get(\"count\")\n\n        // Store previous in ref\n        refs.set(\"previousCount\", current)\n\n        // Update context\n        context.set(\"count\", current + step)\n      },\n      decrement({ context, prop }) {\n        const step = prop(\"step\")\n        const current = context.get(\"count\")\n        context.set(\"count\", current - step)\n      },\n      reset({ context }) {\n        const initial = context.initial(\"count\")\n        context.set(\"count\", initial)\n      },\n      logCount({ context, computed }) {\n        const count = context.get(\"count\")\n        const isEven = computed(\"isEven\")\n        console.log(`Count: ${count} (${isEven ? \"even\" : \"odd\"})`)\n      },\n      notifyChange({ prop, context }) {\n        const count = context.get(\"count\")\n        prop(\"onChange\")?.({ count })\n      },\n      logStepChanged({ prop }) {\n        console.log(\"Step changed to:\", prop(\"step\"))\n      },\n    },\n  },\n})\n```\n\n## TypeScript Guide\n\nTo make your machine type-safe, define a schema interface that describes all the\ntypes:\n\n```tsx\nimport type { EventObject, Machine, Service } from \"@zag-js/core\"\n\n// Define props interface\nexport interface CounterProps {\n  step?: number\n  min?: number\n  max?: number\n  defaultValue?: number\n  value?: number\n  onChange?: (details: { count: number }) => void\n}\n\n// Define the machine schema\nexport interface CounterSchema {\n  state: \"idle\"\n  props: CounterProps\n  context: {\n    count: number\n  }\n  refs: {\n    previousCount: number | null\n    operationCount: number\n  }\n  computed: {\n    isEven: boolean\n    canIncrement: boolean\n    canDecrement: boolean\n  }\n  event: EventObject\n  action: string\n  guard: string\n  effect: string\n}\n\n// Create typed machine\nexport const machine = createMachine<CounterSchema>({\n  // ... machine definition\n})\n\n// Export typed service\nexport type CounterService = Service<CounterSchema>\n```\n\n### Schema Properties\n\n- **`state`**: Union of all possible states (`\"idle\" | \"active\" | \"disabled\"`)\n- **`props`**: Props interface (user configuration)\n- **`context`**: Context values (reactive state)\n- **`refs`**: Refs values (non-reactive references)\n- **`computed`**: Computed values (derived state)\n- **`event`**: Event types (usually `EventObject`)\n- **`action`**: Action names (usually `string`)\n- **`guard`**: Guard names (usually `string`)\n- **`effect`**: Effect names (usually `string`)\n\nThis provides full type safety throughout your machine implementation.","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/guides/building-machines.mdx"}