# DisplayReactor

## Overview

`DisplayReactor` is a specialized reactor that serves as a **bridge between Candid's type system and your user interface**. It automatically transforms IC-native types into formats that are natural to work with in JavaScript and easy to display in UIs.

### The Philosophy

Candid (the IC's interface description language) has its own type system that doesn't always map cleanly to JavaScript:

- `bigint` values can't be displayed directly in templates or serialized to JSON
- `Principal` objects need `.toText()` every time you display them
- Optional values are arrays (`[value]` or `[]`) instead of `value | undefined`
- Variants use object keys instead of discriminators

**DisplayReactor handles all these transformations automatically**, in both directions. You work with clean, JavaScript-native types in your UI, and DisplayReactor converts them to Candid when calling the canister and back to display format when receiving results.

```typescript
import { DisplayReactor } from "@ic-reactor/core"
import { clientManager } from "./clients"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"

// DisplayReactor: the bridge between Candid and your UI
const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})

// Input: JavaScript strings (easy to get from forms)
// Output: Display-friendly types (easy to render)
async function showAccount() {
  const account = await backend.callMethod({
    functionName: "getAccount",
    args: ["aaaaa-aa"], // Principal as string ✓
  })
  console.log(account.balance) // "1000000" (string, not bigint) ✓
}
```

This is one of IC Reactor's most unique and powerful features — it eliminates the need for manual type conversions between the IC and your frontend.

## The Problem

Candid types don't always map directly to JavaScript types that are easy to work with in UIs:

```ts nocheck
// What the canister returns (Candid types)
{
  balance: 1000000000n,              // bigint - can't display directly
  owner: Principal.fromText("..."),  // Principal object - need .toText()
  createdAt: 1703500800000000000n,   // nanoseconds as bigint
  metadata: [["a", 1], ["b", 2]],    // Array of tuples, not a Map
  status: { Active: null },          // Variant with null value
  avatar: Uint8Array([...]),         // Binary data
  description: [],                   // Empty array means none (opt type)
}
```

You'd normally need to write conversion logic everywhere in your app:

```tsx
import type { Account } from "./declarations/backend"

// Without DisplayReactor - tedious and error-prone
function AccountRow({ balance, owner, status, description }: Account) {
  return (
    <>
      <span>Balance: {balance.toString()}</span>
      <span>Owner: {owner.toText()}</span>
      <span>Status: {Object.keys(status)[0]}</span>
      <span>Description: {description[0] ?? "No description"}</span>
    </>
  )
}
```

## The Solution

`DisplayReactor` automatically transforms these types to UI-friendly formats:

```ts nocheck
// What DisplayReactor gives you
{
  balance: "1000000000",                // string - easy to display
  owner: "rrkah-fqaaa-aaaaa-aaaaq-cai", // string - already formatted
  createdAt: "1703500800000000000",     // string - parse as needed
  metadata: { "a": 1, "b": 2 },         // Plain Object - simple access
  status: { _type: "Active" },          // Normalized variant
  avatar: "89504e47...",                // Blobs → hex string, at every size
  description: undefined,               // undefined instead of []
}
```

Now your components are simple:

```tsx
import type { ReactorDataOf } from "@ic-reactor/react"
import type { backend } from "./reactor"

// With DisplayReactor - clean and straightforward
function AccountRow({
  balance,
  owner,
  status,
  description,
}: ReactorDataOf<typeof backend, "getAccount">) {
  return (
    <>
      <span>Balance: {balance}</span>
      <span>Owner: {owner}</span>
      <span>Status: {status._type}</span>
      <span>Description: {description ?? "No description"}</span>
    </>
  )
}
```

## Import

```typescript
import { DisplayReactor } from "@ic-reactor/core"
```

## Usage

### Creating a DisplayReactor

```typescript
import { DisplayReactor } from "@ic-reactor/core"
import { clientManager } from "./clients"
import { idlFactory, type _SERVICE } from "../declarations/backend"

const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
})
```

### With createActorHooks

Pass a `DisplayReactor` instance to `createActorHooks` to get hooks with automatic type transformations:

```typescript
import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
import { clientManager } from "./clients"
import { idlFactory, type _SERVICE } from "../declarations/backend"

// Create a DisplayReactor
const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
})

// Pass it to createActorHooks - all hooks use display types
const { useActorQuery, useActorMutation } = createActorHooks(backend)
```

### With defineDisplayReactor

`defineDisplayReactor` does the whole setup in one call — `QueryClient`,
`ClientManager`, the `DisplayReactor`, its hooks and the Internet Identity
hooks — and takes the same options as `defineReactor`, plus `validators`:

```typescript
import { defineDisplayReactor } from "@ic-reactor/react"
import { idlFactory, type _SERVICE } from "../declarations/backend"

export const {
  reactor: backend,
  useActorQuery,
  useAuth,
} = defineDisplayReactor<_SERVICE>({
  name: "backend",
  idlFactory,
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
})
```

It replaces `defineReactor({ display: true })`, which still works but is
deprecated: the flag makes every `defineReactor` bundle carry `DisplayReactor`
and zod, whether or not it is set.

**Note:** The display codecs are built on zod, which adds about 85 kB minified (24 kB
  gzipped) to a bundle that does not already include it. See [Bundle
  Size](https://ic-reactor.b3pay.net/v3/packages/react#bundle-size) for what each setup path costs.

## Type Transformations

**Numbers**
`bigint` → `string` Large numbers stay precise

**Principals**
`Principal` → `string` Ready for display

**Optionals**
`[T] | []` → `T | undefined` JavaScript-native nullish

**Blobs**
`vec nat8` → hex `string`, at every size

### Complete Transformation Reference

#### Numeric Types

| Candid    | TypeScript | Display  | Notes                                                     |
| --------- | ---------- | -------- | --------------------------------------------------------- |
| `nat`     | `bigint`   | `string` | Arbitrary precision preserved                             |
| `int`     | `bigint`   | `string` | Arbitrary precision preserved                             |
| `nat64`   | `bigint`   | `string` | 64-bit integers as strings                                |
| `int64`   | `bigint`   | `string` | 64-bit integers as strings                                |
| `nat8`    | `number`   | `number` | No change needed                                          |
| `nat16`   | `number`   | `number` | No change needed                                          |
| `nat32`   | `number`   | `number` | No change needed                                          |
| `float32` | `number`   | `number` | `NaN`, `±Infinity`, `-0` kept; [JSON loses them](#floats) |
| `float64` | `number`   | `number` | `NaN`, `±Infinity`, `-0` kept; [JSON loses them](#floats) |

#### Identity & Binary Types

| Candid      | TypeScript   | Display  | Notes                       |
| ----------- | ------------ | -------- | --------------------------- |
| `principal` | `Principal`  | `string` | e.g., `"rrkah-fqaaa..."`    |
| `blob`      | `Uint8Array` | `string` | Hex, no `0x`: `"89504e..."` |

#### Container Types

| Candid           | TypeScript      | Display                | Notes                                              |
| ---------------- | --------------- | ---------------------- | -------------------------------------------------- |
| `opt T`          | `[T] \| []`     | `T \| undefined`       | None is `undefined`; input also takes `null`       |
| `vec T`          | `T[]`           | `Display<T>[]`         | Elements are transformed                           |
| `vec (text, T)`  | `[string, T][]` | `{ [key: string]: T }` | [One value per key](#working-with-key-value-pairs) |
| `record { ... }` | `{ ... }`       | `{ ... }`              | Nested fields transformed                          |

#### Variant Types

| Candid             | TypeScript    | Display                         | Notes                                 |
| ------------------ | ------------- | ------------------------------- | ------------------------------------- |
| `variant { A }`    | `{ A: null }` | `{ _type: "A" }`                | Normalized with `_type` discriminator |
| `variant { A: T }` | `{ A: T }`    | `{ _type: "A", A: Display<T> }` | Value field preserved                 |

## Bidirectional Transformation

DisplayReactor transforms data in **both directions**:

1. **Arguments (Display → Candid)**: When you call a method, your display-friendly args are converted to Candid format
2. **Results (Candid → Display)**: When the canister responds, Candid data is converted to display format

```typescript
async function showAccount() {
  // You provide display-friendly input
  const account = await backend.callMethod({
    functionName: "getAccount",
    args: ["aaaaa-aa"], // Principal as string ✓
  })

  // You receive display-friendly output
  console.log(account.balance) // "1000000" (string, not bigint)
  console.log(account.createdAt) // "1703500800000000000" (string)
}
```

If an argument cannot be converted — a malformed principal string, a
non-numeric amount — the call **throws** instead of passing the untransformed
display value on to Candid encoding, so the diagnostic points at the bad field
rather than surfacing as a generic `IDL.encode` error raised far from the real
cause. `callMethod` rejects with a `CallError`; its `cause` is the conversion
error naming what failed to convert, and the original codec failure sits one
level deeper as that error's own `cause`.

## Automatic Result Unwrapping

IC Reactor automatically unwraps Candid `Result` types (`variant { Ok: T; Err: E }`):

- **On `Ok`**: Returns the success value directly (with display transformations applied)
- **On `Err`**: Throws a `CanisterError` containing the error value

```typescript
import { CanisterError } from "@ic-reactor/core"

// Canister method signature:
// createUser : (CreateUserInput) -> (Result<User, CreateUserError>)

// Without IC Reactor, you'd need to handle:
// { Ok: User } | { Err: CreateUserError }

// With IC Reactor - automatic unwrapping:
async function createUser() {
  try {
    const user = await backend.callMethod({
      functionName: "createUser",
      args: [{ name: "Alice", email: "alice@example.com" }],
    })
    // user is directly the User object (not { Ok: User })
    console.log(user.name) // "Alice"
  } catch (error) {
    if (error instanceof CanisterError) {
      // error.err is the CreateUserError in display form: a variant carries
      // a `_type`, so `{ EmailAlreadyExists: null }` arrives as
      // `{ _type: "EmailAlreadyExists" }`
      if (error.err._type === "EmailAlreadyExists") {
        console.log("Email taken!")
      }
    }
  }
}
```

This means you never have to manually check for `Ok` or `Err` variants — IC Reactor handles it for you.

**Tip:** Learn more about handling canister errors in the [Error
  Handling](https://ic-reactor.b3pay.net/v3/guides/error-handling) guide.

## Examples

### Basic Usage

```typescript
import { DisplayReactor } from "@ic-reactor/core"
import { clientManager } from "./clients"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"

const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})

async function getBalance() {
  // All returned values use display types
  const balance = await backend.callMethod({
    functionName: "getBalance",
    args: ["aaaaa-aa"], // Principal as string
  })
  // balance is string, not bigint
  return balance
}
```

### Many Canisters of One Interface

Like `Reactor`, `DisplayReactor` gives a sibling for another canister of the
same interface with [`forCanister`](https://ic-reactor.b3pay.net/v3/reference/reactor#forcanister). This is
the multi-token wallet pattern: one reactor per token, all on the same agent
and cache, with the display transform and validators passed on:

```typescript
import { DisplayReactor } from "@ic-reactor/core"
import { clientManager } from "./clients"
import { idlFactory, type _SERVICE } from "./declarations/icrc1"

// The reactor the siblings are made from. It still needs a canister ID of its
// own: the constructor throws without one outside a local replica.
const tokenReactor = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "token",
  canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP ledger
})

async function getTokenInfo(canisterId: string) {
  // The same reactor each time for the same canister
  const token = tokenReactor.forCanister(canisterId)

  // Two calls for two tokens can run at once without crossing
  const [name, symbol, decimals] = await Promise.all([
    token.callMethod({ functionName: "icrc1_name" }),
    token.callMethod({ functionName: "icrc1_symbol" }),
    token.callMethod({ functionName: "icrc1_decimals" }),
  ])

  // name and symbol are text; decimals, a nat8, is a number
  return { name, symbol, decimals }
}
```

`setCanisterId` still retargets one reactor in place, for code that follows a
single selection, but every call and view built on that reactor moves with it.

### With React Hooks

```tsx
import {
  DisplayReactor,
  createActorHooks,
  formatTokenAmount,
} from "@ic-reactor/react"
import { clientManager } from "./clients"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"

// Create DisplayReactor and hooks
const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})
const { useActorQuery } = createActorHooks(backend)

function Balance({ principal }: { principal: string }) {
  const { data: balance } = useActorQuery({
    functionName: "getBalance",
    args: [principal], // string, not Principal
  })

  // balance is the nat as text, in e8s. Format it exactly; Number(balance) / 1e8
  // loses digits.
  if (balance === undefined) return <span>…</span>
  return <span>{formatTokenAmount(balance, 8)} ICP</span>
}
```

See [Token amounts](https://ic-reactor.b3pay.net/v3/reference/utilities#token-amounts) for
`formatTokenAmount` and `parseTokenAmount`.

### Handling Variants

Candid variants are normalized for easier pattern matching:

```tsx
// Candid variant: variant { Pending; Active; Completed: nat }
// Raw TS: { Pending: null } | { Active: null } | { Completed: bigint }

// With DisplayReactor:
type DisplayStatus =
  | { _type: "Pending" }
  | { _type: "Active" }
  | { _type: "Completed"; Completed: string }

function StatusBadge({ status }: { status: DisplayStatus }) {
  switch (status._type) {
    case "Pending":
      return <span className="badge yellow">Pending</span>
    case "Active":
      return <span className="badge green">Active</span>
    case "Completed":
      return <span className="badge blue">Done at {status.Completed}</span>
  }
}
```

### Handling Optionals

No more array-based optional checking:

```typescript
import type { ReactorDataOf } from "@ic-reactor/react"
import type { User } from "./declarations/backend"
import type { backend } from "./reactor"

// Without DisplayReactor
function readRawUser(user: User) {
  const description = user.bio[0] ?? "No bio"
  const avatar = user.avatar.length > 0 ? user.avatar[0] : null
  return { description, avatar }
}

// With DisplayReactor
function readUser(user: ReactorDataOf<typeof backend, "getUser">) {
  const description = user.bio ?? "No bio"
  const avatar = user.avatar // Already undefined or value
  return { description, avatar }
}
```

### Handling Blobs (Binary Data)

Every blob (`vec nat8`) decodes to a lowercase hex string with no `0x`
prefix, whatever its size. One Candid type, one JavaScript type — hex text is
always JSON-safe, so persisting or serialising a display value never corrupts
binary fields.

**Changed behavior — was size-dependent:** Releases up to and including v3.10.0 decode only blobs of 512 bytes or fewer
  to hex; anything larger stays a `Uint8Array`, so one field could change JS
  type with payload size and JSON-serialising a result silently corrupted large
  blobs. Every release after v3.10.0 decodes all blobs to hex. If you branched
  on `typeof blob === "string"`, the `Uint8Array` branch is now dead — delete
  it, or convert back with the exported `hexToUint8Array(value)`. If you were
  relying on raw bytes for large payloads (hex doubles the in-memory size), read
  them through a plain `Reactor`, which leaves blobs untransformed.

```typescript
import { hexToUint8Array } from "@ic-reactor/core"
import type { ReactorDataOf } from "@ic-reactor/react"
import { useActorMutation, type backend } from "./reactor"

// Candid: blob (vec nat8)
export function useBlobs(
  account: ReactorDataOf<typeof backend, "getAccount">,
  tx: ReactorDataOf<typeof backend, "getTransaction">
) {
  // Every blob is a hex string (hash, signature, icon, image — any size)
  const hash = tx.hash // "2cf24dba5fb0a30e..."
  const signature = tx.signature // "3045022100ab..."
  const imageData = account.avatar // "89504e470d0a1a0a..." (hex too)

  // Need the bytes back? hexToUint8Array is exported from the package.
  const bytes = hexToUint8Array(imageData)

  // When sending blobs back, you can use either format:
  const { mutate } = useActorMutation({ functionName: "saveDocument" })
  const save = () =>
    mutate([
      {
        // Hex string input (DisplayReactor converts to Uint8Array)
        hash: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c",

        // Or Uint8Array directly
        data: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
      },
    ])

  return { hash, signature, bytes, save }
}
```

### Floats

`float32` and `float64` display as JavaScript numbers, and every value a
canister can return, `NaN`, `Infinity`, `-Infinity` and `-0` included, is sent
back as the same bytes. As an argument a float is also taken as the text a form
holds, such as `"3.14"`, which must spell a finite number: `""`, `"abc"`,
`"Infinity"` and `"NaN"` are refused, as the form schemas of `@ic-reactor/candid`
refuse them. A finite number that `float32` cannot hold, such as `3.5e38`, is
refused too, since Candid encoding would send it as `Infinity`.

**JSON cannot hold every float:** `JSON.stringify` writes `NaN`, `Infinity` and `-Infinity` as `null`, and `-0`
  as `0`. After a JSON round trip, such as a persisted query cache, SSR
  hydration or a value posted to your server, an `opt float64` that held `NaN`
  comes back as `null` and is sent as none, a required float comes back as
  `null` and the call is refused, and `-0` is sent as `0`. Where such values can
  occur (a rate computed as 0/0, for example), keep them out of JSON or
  serialise them yourself. Query keys are not affected: they write each of these
  values apart from every other.

### Working with Key-Value Pairs

Candid's `vec (text, T)` becomes a plain JavaScript Object:

```typescript
// Without DisplayReactor
const rawMetadata: Array<[string, string]> = [
  ["key1", "val1"],
  ["key2", "val2"],
]
const rawValue = rawMetadata.find(([k]) => k === "key1")?.[1]

// With DisplayReactor
const metadata = {
  key1: "val1",
  key2: "val2",
}
const value = metadata["key1"]
```

As an argument, the map is also taken as the array of pairs or as a `Map`.
Each is sent as its entries in its own order: an object's, the array's, or the
`Map`'s insertion order. The argument's TypeScript type names only the object,
so the other two need a cast.

**An object holds each key once, in its own order:** A JavaScript object cannot hold a key twice, and it puts integer-like keys
  (`"0"` to `"4294967294"`) first, in ascending order, before every other key.
  So when a result's `vec record { text; T }` becomes an object, a repeated key
  keeps only its last value (an HTTP response's second `Set-Cookie` header
  replaces the first), and integer-like keys move to the front: `10, 2, b, 1`
  displays as `1, 2, 10, b`. Sending that object back sends the vector it
  holds, not the one read. Where repeated keys or their order matter, such as
  HTTP headers, ICRC-21 consent message fields or ICRC-3 maps with numeric
  keys, call the method through a plain [`Reactor`](https://ic-reactor.b3pay.net/v3/reference/reactor),
  which returns the array of pairs as received. A `DisplayReactor` argument
  given as the array of pairs, with a cast, is sent as it is.

### Form Handling

Display types work seamlessly with form inputs:

```tsx
import { useState } from "react"
import { useActorMutation } from "./reactor"

function TransferForm() {
  const [recipient, setRecipient] = useState("")
  const [amount, setAmount] = useState("")

  const { mutate } = useActorMutation({
    functionName: "transfer",
  })

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    // Strings work directly - no BigInt conversion needed!
    mutate([{ to: recipient, amount }])
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        placeholder="Principal ID"
      />
      <input
        type="text"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Amount"
      />
      <button type="submit">Transfer</button>
    </form>
  )
}
```

This amount field takes base units (e8s). A field a person types a token
amount into, such as `"1.5"`, is read with
[`parseTokenAmount`](https://ic-reactor.b3pay.net/v3/reference/utilities#parsetokenamount):
`mutate([{ to: recipient, amount: parseTokenAmount(amount, decimals).toString() }])`. It throws
on text it cannot read exactly instead of rounding it.

### Accessing the Codec

For advanced use cases, access the underlying codec:

```typescript
import type { Account } from "./declarations/backend"

function convertManually(displayArg: string, candidResult: Account) {
  const codec = backend.getCodec("getAccount")

  if (codec) {
    // Transform display → candid manually. A method with one argument takes
    // that argument itself, one with several a tuple, and one with none `null`
    const candidArg = codec.args.asCandid(displayArg)

    // Transform candid → display manually
    const displayResult = codec.result.asDisplay(candidResult)
  }
}
```

## Type Safety

DisplayReactor maintains full type safety. TypeScript knows the transformed types:

```tsx
import { useActorQuery } from "./reactor"

function AccountBalance({ owner }: { owner: string }) {
  // TypeScript infers the correct display types
  const { data } = useActorQuery({
    functionName: "getAccount",
    args: [owner], // string (was Principal)
  })

  // data.balance is string (was bigint)
  // data.createdAt is string (was bigint)
  // data.description is string | null | undefined (was [string] | []); none arrives as undefined
  return <span>{data?.balance}</span>
}
```

The display types come from the service type parameter
(`new DisplayReactor<_SERVICE>(...)`). Without one, the reactor is typed by
`BaseActor` and knows nothing about the Candid, so every method's display-side
arguments and results are `unknown`: validators receive `unknown` args, and
results must be narrowed before they are rendered. (An untyped `Reactor` types
the same values as `any`, because the raw side passes `BaseActor`'s `any`
through.) Pass the service type from your generated declarations, or cast to
the display shape you expect where the interface is only known at run time, as
with a [`CandidDisplayReactor`](https://ic-reactor.b3pay.net/v3/packages/candid/candiddisplayreactor#types-with-and-without-a-service-type).

## Validation

DisplayReactor includes optional argument validation. Validators receive **display types**,
making them perfect for form validation.

### Registering Validators

```typescript
import { DisplayReactor } from "@ic-reactor/core"
import { clientManager } from "./clients"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"

const backend = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})

// Validators receive display types (strings!)
backend.registerValidator("transfer", ([input]) => {
  const issues = []

  // input.to is string (not Principal)
  if (!input.to || input.to.length === 0) {
    issues.push({ path: ["to"], message: "Recipient is required" })
  }

  // input.amount is string (not bigint)
  if (!/^\d+$/.test(input.amount)) {
    issues.push({ path: ["amount"], message: "Must be a valid number" })
  }

  return issues.length > 0 ? { success: false, issues } : { success: true }
})
```

### Pre-submission Validation

Use `validate()` to check before calling the canister:

```typescript
async function submit() {
  const result = await backend.validate("transfer", [formData])

  if (!result.success) {
    result.issues.forEach((issue) => {
      form.setError(issue.path[0], issue.message)
    })
    return
  }

  // Validation passed, make the call
  await backend.callMethod({ functionName: "transfer", args: [formData] })
}
```

### Zod Integration

Use `fromZodSchema` for type-safe validation:

```typescript
import { z } from "zod"
import { fromZodSchema, DisplayReactor } from "@ic-reactor/core"

const transferSchema = z.object({
  to: z.string().min(1, "Recipient is required"),
  amount: z.string().regex(/^\d+$/, "Must be a valid number"),
})

backend.registerValidator("transfer", fromZodSchema(transferSchema))
```

The schema checks the method's first argument. zod refuses a schema with an
async refinement or transform on its synchronous path, so pass
`{ async: true }` for one: the validator then parses with `safeParseAsync` and
returns a promise, and runs where async validators run (below).

```typescript
import { z } from "zod"
import { fromZodSchema } from "@ic-reactor/core"

const recipientSchema = z.object({
  to: z
    .string()
    .refine(async (to) => !(await isBlocked(to)), "Address is blocked"),
})

backend.registerValidator(
  "transfer",
  fromZodSchema(recipientSchema, { async: true })
)
```

### Async Validators

For async validation (e.g., checking a blocklist), use `validate()` or
`callMethodWithValidation`:

```typescript
backend.registerValidator("transfer", async ([input]) => {
  const isBlocked = await checkBlocklist(input.to)
  if (isBlocked) {
    return {
      success: false,
      issues: [{ path: ["to"], message: "Address is blocked" }],
    }
  }
  return { success: true }
})

// Use callMethodWithValidation for async validators
async function submit() {
  await backend.callMethodWithValidation({
    functionName: "transfer",
    args: [formData],
  })
}
```

`callMethod()` runs validators synchronously, and so do the query and mutation
hooks and factories that call it. They refuse a call whose validator returns a
promise, with a `CallError` that names the method, without sending anything.

### When a Validator Throws

A validator that throws or rejects (a blocklist lookup that fails, say) is
reported the same way by `callMethod()`, `callMethodWithValidation()` and
`validate()`: as a `CallError` whose `cause` is what the validator threw, and
the canister is not called. It is not a verdict on the arguments, so it is not
a `ValidationError`, and it is not the canister's answer, so a `CanisterError`
the validator's own lookup threw is not passed on as one. A `ValidationError`
the validator throws is passed on as it is.

```typescript
import { isCallError, isValidationError } from "@ic-reactor/core"

async function submit() {
  try {
    await backend.callMethodWithValidation({
      functionName: "transfer",
      args: [formData],
    })
  } catch (error) {
    if (isValidationError(error)) {
      // the validator's verdict: error.issues
    } else if (isCallError(error)) {
      // a failed call, or a validator that threw: error.cause
    }
  }
}
```

**Tip:** Validation is optional! If you don't register validators, DisplayReactor works
  exactly like before — just type transformations.

## When to Use DisplayReactor

✅ **Use DisplayReactor when:**

- Building user interfaces
- Working with forms
- Displaying data in components
- Serializing data to JSON (mind [non-finite floats](#floats))
- You want simpler type handling
- You need client-side validation

❌ **Use regular Reactor when:**

- Performing arithmetic with bigint values
- Building backend services
- You need raw Candid types for specific operations
- You're serializing to specific binary formats

## Notes

**Tip:** Use `DisplayReactor` with `createActorHooks` for UI-friendly types. Just pass
  a DisplayReactor instance to get hooks with automatic type transformations.

**Note:** Display transformations happen automatically on every call. There's no
  performance penalty for the transformation itself.

**Caution:** If you need to perform math with large numbers, convert back to BigInt:
  `BigInt(balance)`. To show a token amount or read one a person typed, use
  `formatTokenAmount` and `parseTokenAmount`, never `Number`.

## See Also

- [Reactor](https://ic-reactor.b3pay.net/v3/reference/reactor) — Standard reactor without transformations
- [Type Safety](https://ic-reactor.b3pay.net/v3/guides/type-safety) — Type system details
- [createActorHooks](https://ic-reactor.b3pay.net/v3/reference/createactorhooks/overview) — Create React hooks from a reactor
- [React Validation](https://ic-reactor.b3pay.net/v3/reference/reactvalidation) — React validation utilities
- [Utilities](https://ic-reactor.b3pay.net/v3/reference/utilities) — Exact token-amount formatting and parsing
- [Error Handling](https://ic-reactor.b3pay.net/v3/guides/error-handling) — Handle validation and canister errors