---
title: React
description: Headless hooks that run the custom-domain lifecycle over a transport your server owns, and the shadcn registry that renders them in your design system.
sidebar:
  label: Overview
seo:
  title: DomainKit React hooks
---

`@domainkit/react` runs the whole custom-domain lifecycle over a transport your server owns, and
paints none of it. The package supplies state, copy, and accessibility; your application supplies
the markup. Use it to add custom-domain setup to a React settings page. Provider credentials never reach the browser: the transport calls the routes you
mounted from `domainkit/server`.

```package-install
@domainkit/react domainkit effect react react-dom
```

React 19 is required. Install `domainkit` and `@domainkit/react` at the same release version.

## One hook

<Snippet
  file="examples/react/domain-settings.tsx"
  region="setup"
  title="The transport and the requirements"
/>

<Snippet file="examples/react/domain-settings.tsx" region="flow" title="The whole flow" />

`Domain.useFlow` connects a provider, plans the DNS changes, takes the customer's approval or
refusal, applies the plan, observes the records, and cleans them up against the apply receipt. It
returns the four controllers, the plan the rows report, the latest readiness, the capability groups
the transport declares, and a `FlowState` your own surface reads.

Writing `Transport.fromFetch("/api/domainkit")` inline in JSX is fine: `DomainKit.Root` keeps the
transport's identity for the whole mount, so the controllers do not restart on every render.

## The styled path

The [registry](/components/registry) vends the flow as shadcn components written against your own
`ui/button`, `ui/dialog`, `ui/table`, and `ui/badge`, on the Base UI idiom.

```package-install
npx shadcn@latest add https://domain-kit.dev/r/domain-flow.json
```

The files land in `components/domainkit/` and are yours after the copy. A project without a shadcn
kit composes the hooks directly.

## Two ways in

| Level     | You write                            | Reach for it when                                      |
| --------- | ------------------------------------ | ------------------------------------------------------ |
| The block | `<DomainFlow domain requirements />` | The product needs a working screen today               |
| The hooks | `useFlow` or the four controllers    | The lifecycle belongs inside a screen you already have |

[Composition](/docs/react/composition) covers the surface you write over the flow,
[controllers](/docs/react/controllers) the hooks behind it, and [words](/docs/react/presentation)
the message catalog every string comes from.

## Events

<Snippet
  file="examples/react/domain-settings.tsx"
  region="events"
  title="Observing finished steps"
/>

`onEvent` fires when a customer finishes something: `Connected`, `Applied`, `Cleaned`, `Declined`,
`Detached`, `Disconnected`, and `Failed`. A listener that throws cannot change the outcome of an
operation that already completed.

## Keeping the UI honest

Pass `revision` and bump it after a change the UI did not make, such as a webhook that disconnected
a provider. Every mounted domain re-inspects.

## Next.js

The package entry carries `"use client"`. A transport is an object of functions, so build it inside
a client module rather than passing one down from a server component.

## The member view

A customer who may read a domain but not change it gets `readOnly`. Every command that would start
a write refuses to run, and the flag rides on `flow.state.readOnly`, so your surface can say who may
connect instead of rendering an empty row.

<Snippet file="examples/react/read-only.tsx" region="root" title="Read-only for the whole tree" />

<Snippet file="examples/react/read-only.tsx" region="per-domain" title="One domain among several" />

Capability gating is the other half: a group the transport never declares is absent from
`flow.capabilities`. `readOnly` covers the authorization a transport cannot express, such as a
member of an organisation who reaches the same routes and would get a 403 from any write.

Observation and verification retries stay available, because checking DNS reads the world rather
than changing the domain. Provisioning and cleanup retries are refused. Connection retries
re-inspect instead of resending a failed write.

## Verification without a connection

The flow observes the requirements it was given, so a domain with no provider attached still reports
which records are in place instead of failing on the missing receipt.

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

## Where an interactive provider returns

<Snippet file="examples/react/domain-settings.tsx" region="return-to" title="Naming the return" />

`returnTo` defaults to the page the customer started from, read when they connect rather than when
the flow renders. Pass `null` to send none and leave the server's `defaultReturnTo` in charge.

## Next

**[Registry](/components/registry)**

The styled flow, with a preview that runs the real lifecycle.

**[@domainkit/react reference](/docs/reference/react)**

Namespaces, hooks, controller states, and options.

**[Adding a domain](/docs/react/composition)**

The zone picker, and a surface of your own over the flow.

**[The transport](/docs/react/transport)**

What the browser calls, and what it never holds.
