# 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
// 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)
const user = await backend.callMethod({
  functionName: "getUser",
  args: ["aaaaa-aa"], // Principal as string ✓
})
console.log(user.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:

```typescript
// 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
// Without DisplayReactor - tedious and error-prone
<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:

```typescript
// 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
// With DisplayReactor - clean and straightforward
<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 "./client"
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 "./client"
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)
```

## Type Transformations

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

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

**Optionals**
`[T] | []` → `T | null` 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` | No change needed              |
| `float64` | `number`   | `number` | No change needed              |

#### 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 }` | Key-value pairs as Object                    |
| `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
// You provide display-friendly input
const user = await backend.callMethod({
  functionName: "getUser",
  args: ["aaaaa-aa"], // Principal as string ✓
})

// You receive display-friendly output
console.log(user.balance) // "1000000" (string, not bigint)
console.log(user.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
// 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:
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 contains the CreateUserError
    if ("EmailAlreadyExists" in error.err) {
      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"

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

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

### Dynamic Canister Switching

Like `Reactor`, `DisplayReactor` supports dynamic canister switching via `setCanisterId`. This is particularly useful for multi-token wallets:

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

// Create a reusable ICRC-1 token reactor. It still needs a canister ID to
// start from: the constructor throws without one outside a local replica,
// before any setCanisterId call below can run.
const tokenReactor = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "token",
  canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP ledger until switched
})

async function getTokenInfo(canisterId: string) {
  // Switch to the target token canister
  tokenReactor.setCanisterId(canisterId)

  // Queries and mutations now target the new canister
  const [name, symbol, decimals] = await Promise.all([
    tokenReactor.callMethod({ functionName: "icrc1_name" }),
    tokenReactor.callMethod({ functionName: "icrc1_symbol" }),
    tokenReactor.callMethod({ functionName: "icrc1_decimals" }),
  ])

  return { name, symbol, decimals } // All display types (strings)
}
```

### With React Hooks

```typescript
// 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 string, ready for display
  return <span>{balance} ICP</span>
}
```

### Handling Variants

Candid variants are normalized for easier pattern matching:

```typescript
// 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
// Without DisplayReactor
const description = user.bio[0] ?? "No bio"
const avatar = user.avatar.length > 0 ? user.avatar[0] : null

// With DisplayReactor
const description = user.bio ?? "No bio"
const avatar = user.avatar // Already null or value
```

### 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 — a display
value is always JSON-safe, so persisting or serialising it 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
// Candid: blob (vec nat8)

// Every blob is a hex string (hash, signature, icon, image — any size)
const hash = user.passwordHash // "2cf24dba5fb0a30e..."
const signature = tx.signature // "3045022100ab..."
const imageData = user.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:
mutate([
  {
    // Hex string input (DisplayReactor converts to Uint8Array)
    hash: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c",

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

### Working with Key-Value Pairs

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

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

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

### Form Handling

Display types work seamlessly with form inputs:

```tsx
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([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>
  )
}
```

### Accessing the Codec

For advanced use cases, access the underlying codec:

```typescript
const codec = backend.getCodec("getUser")

if (codec) {
  // Transform display → candid manually
  const candidArgs = codec.args.asCandid(displayArgs)

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

## Type Safety

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

```typescript
// TypeScript infers the correct display types
const { data } = useActorQuery({
  functionName: "getUser",
  args: [userId], // string (was Principal)
})

// data.balance is string (was bigint)
// data.createdAt is string (was bigint)
// data.bio is string | null (was [string] | [])
```

## 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"

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
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))
```

### Async Validators

For async validation (e.g., checking a blocklist), use `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
await backend.callMethodWithValidation({
  functionName: "transfer",
  args: [formData],
})
```

**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
- 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)`

## 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
- [Error Handling](https://ic-reactor.b3pay.net/v3/guides/error-handling) — Handle validation and canister errors