---
title: Controllers
description: The four hooks behind the flow, the tagged states they expose, the commands they take, and how an attempt is abandoned when its inputs change.
sidebar:
  label: Controllers
seo:
  title: DomainKit React controllers
---

Every part of the flow is a hook. Each takes one options object and returns a controller whose
`state` is a tagged union, so a screen renders from `state._tag` and never from a boolean pair.

<Snippet file="examples/react/controllers.tsx" region="hooks" title="The four controllers" />

## Connection

`Connect.useController({ domain, returnTo? })`

| State               | Means                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `Loading`           | Inspecting the domain                                                                     |
| `Disconnected`      | No connection. Carries `discovery` when one of the owner's connections reaches the domain |
| `Connected`         | Attached and usable                                                                       |
| `Reconnect`         | The credential can no longer be refreshed                                                 |
| `Submitting`        | A connect, attach, detach, or disconnect is in flight                                     |
| `Redirecting`       | Sending the customer to the provider                                                      |
| `SelectionRequired` | The credential reaches several matching zones                                             |
| `Failure`           | Carries the `DomainKit.Error`                                                             |

| Command                                             | Does                                                      |
| --------------------------------------------------- | --------------------------------------------------------- |
| `connect({ provider, method, values?, returnTo? })` | Starts a connection                                       |
| `reuse({ connectionId, zone? })`                    | Attaches the domain to a connection the owner already has |
| `select(zone)`                                      | Answers `SelectionRequired`                               |
| `detach()` / `disconnect()`                         | Releases the domain, or the whole connection              |
| `refresh()` / `retry()`                             | Re-inspects, or re-runs the step that failed              |

Discovery runs on mount whenever the transport declares it and the domain has no connection yet. A
discovery failure leaves `discovery` null and the provider list still renders: discovery is an
optimisation, not a step the customer asked for.

`returnTo` is where an interactive method sends the customer back to. It defaults to the page they
started from, read when they connect rather than when the controller renders, so a flow mounted on
one screen and used on another still returns to the right place. Pass `null` to send none and leave
the server's `defaultReturnTo` in charge; a per-call `connect({ returnTo })` still wins.

## Provisioning and cleanup

`Provision.useController({ domain, requirements, onApplied? })` and
`Cleanup.useController({ domain, receiptId?, onCleaned? })` run the same machine.

| State       | Means                                      |
| ----------- | ------------------------------------------ |
| `Idle`      | Nothing planned yet                        |
| `Planning`  | Building the plan                          |
| `Planned`   | Carries the plan, waiting for the customer |
| `Approving` | Recording consent                          |
| `Applying`  | Writing                                    |
| `Applied`   | Carries the receipt, complete or partial   |
| `Rejecting` | Recording a refusal                        |
| `Rejected`  | Carries the closed attempt                 |
| `Failure`   | Carries the `DomainKit.Error`              |

<Snippet file="examples/react/controllers.tsx" region="commands" title="Approve or decline" />

`approve(operationIds?)` authorizes the digest and applies it in one customer action, because the
review screen offers Approve and Decline rather than Approve and then Apply. `apply()` stays
available for a host that approves out of band. `reject(reason?)` is terminal: approving that plan
afterwards fails `Stale`.

`retry()` builds a new plan when the reason says the old one is gone (`Stale`, `Expired`,
`Conflict`) and re-runs the failed step otherwise.

## Verification

`Verify.useController({ domain, polling?, requirements? })` observes on mount, then re-observes at
the readiness row's own `nextCheckAt` while it stays mounted. `readiness` holds the latest
observation even while a new one runs, so the table does not blink.

`requirements` says what to look for. Without it the server uses the attachment's latest
provisioning receipt, so a domain with nothing attached has nothing to observe; with it, that domain
still reports which records are in place. The set identifies itself by content, so writing the array
inline does not send the mount effect observing in a loop.

<Snippet
  file="examples/react/read-only.tsx"
  region="unattached"
  title="Observing an unattached domain"
/>

A requirement that is not satisfied renders what it expected, what each observer found for that
name, and the observer's own detail line. An observer whose status is `unknown` never answered, so
it reports no values, and host evidence carries none either.

<ReactExample story="verification" />

## Abandoning an attempt

A controller's plan, approval, and receipt only mean something for the inputs that produced them, so
changing `domain` or `requirements` drops them. The identity is the content of the requirement
records, not array identity, so writing `requirements={[...]}` inline does not lose a plan the
customer is reading.

Every command carries the key it was raised for and refuses once that key has moved, so a command
raised from a layout effect in the same commit cannot act on the previous domain.

## Failures

<Snippet file="examples/react/controllers.tsx" region="failure" title="Rendering a failure" />

The failed state carries the `DomainKit.Error` itself, so a host reads `reason`, `category`, and
`isRetryable` instead of parsing text. `Messages.failure(error, catalog)` picks the sentence.

## Capabilities and permission

<Snippet
  file="examples/react/controllers.tsx"
  region="capabilities"
  title="Rendering only what the server serves"
/>

Capabilities say what the server can serve. `readOnly` says what this customer may do with it, which
no transport can express. A part of your own reads it through `DomainKit.useReadOnly()`, and
`DomainKit.ReadOnly` narrows one subtree without touching the rest of the page.

<Snippet file="examples/react/read-only.tsx" region="hook" title="Asking which mode a part is in" />

Read-only removes a write surface rather than disabling it, and `retry` goes with it: re-running a
failed write is still a write. Re-inspecting and observing stay, because both only read.
