Skip to content
IC Reactor

CandidDisplayReactor

CandidDisplayReactor combines the dynamic Candid capabilities of CandidReactor with the automatic display transformations of DisplayReactor. It provides the best of both worlds: runtime Candid parsing and human-friendly type conversions.

This class is ideal for building UIs that interact with dynamic canisters:

  • Display transformations: bigintstring, Principalstring, optionals unwrapped
  • Dynamic Candid parsing: Initialize from Candid source or fetch from network
  • Optional validation: Register validators for form inputs
  • All standard Reactor methods: callMethod(), fetchQuery(), invalidateQueries(), setCanisterId()
const reactor = new CandidDisplayReactor({
name: "my-dynamic-actor",
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
})
await reactor.initialize() // Fetch IDL from network
// Results are already display-friendly (strings, not bigint)!
const balance = await reactor.callMethod({
functionName: "icrc1_balance_of",
args: [{ owner: "aaaaa-aa" }], // Principal as string!
})
console.log(balance) // "1000000" (string, not bigint)
import { CandidDisplayReactor } from "@ic-reactor/candid"
new CandidDisplayReactor<A = BaseActor, T extends TransformKey = "display">(
config: CandidDisplayReactorParameters
)
Parameter Type Required Description
clientManager ClientManager Yes Client manager from @ic-reactor/core
name string Yes Required for logging & environment lookup
canisterId CanisterId No Resolved from the ic_env cookie when omitted, on a trusted host only
candid string No Candid service definition (avoids network fetch)
idlFactory InterfaceFactory No IDL factory (if already available)
adapter CandidAdapter No Reuse an existing CandidAdapter instance
funcClass { methodName: string; func: IDL.FuncClass } No Register a single IDL.FuncClass (no initialize())
pollingOptions PollingOptions No Update-call polling configuration
validators Partial<Record<FunctionName<A>, DisplayValidator<A, M>>> No Initial validators for method arguments
const reactor = new CandidDisplayReactor({
name: "dynamic-actor",
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
})
// Fetches Candid from canister metadata
await reactor.initialize()
// Now call with display types
const balance = await reactor.callMethod({
functionName: "icrc1_balance_of",
args: [{ owner: "aaaaa-aa" }], // string, not Principal!
})
// balance is "1000000" (string, not bigint)

CandidDisplayReactor automatically converts Candid types to display-friendly formats:

Candid Type Display Type Example
nat, int string 1000000n"1000000"
nat64, int64 string 1234567890n"1234567890"
principal string Principal.fromText(...)"aaaaa-aa"
opt T T | null [value]value, []null
blob string Hex-encoded string (no 0x prefix)

Fetch or parse Candid and update the service definition with display codecs.

await reactor.initialize()

If candid was provided in the constructor, it parses that. Otherwise, fetches from the network.


Register a single method by its Candid signature, with display codec generation.

await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})
// Now use with display types!
const balance = await reactor.callMethod({
functionName: "icrc1_balance_of",
args: [{ owner: "aaaaa-aa" }], // string input
})
// balance is string (not bigint)

Register multiple methods at once.

await reactor.registerMethods([
{ functionName: "icrc1_name", candid: "() -> (text) query" },
{ functionName: "icrc1_symbol", candid: "() -> (text) query" },
{
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
},
])

Validation (Inherited from DisplayReactor)

Section titled “Validation (Inherited from DisplayReactor)”

Add form validation that works with display types:

// Register a validator
reactor.registerValidator("transfer", ([input]) => {
const issues = []
// input.to is string (not Principal)
if (!input.to) {
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 }
})
// Validation runs automatically on callMethod()
try {
await reactor.callMethod({
functionName: "transfer",
args: [{ to: "", amount: "abc" }],
})
} catch (err) {
// ValidationError with issues
}
Method Description
registerValidator(name, fn) Register a validator for a method
unregisterValidator(name) Remove a validator
hasValidator(name) Check if a validator exists
validate(name, args) Validate without calling the canister
callMethodWithValidation() Call with async validator support

For quick calls without explicit registration:

Perform a dynamic query with display transformations.

const balance = await reactor.queryDynamic<string>({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
args: [{ owner: "aaaaa-aa" }], // Display types!
})
// balance is "1000000" (string)

Perform a dynamic update call with display transformations.

const result = await reactor.callDynamic({
functionName: "transfer",
candid:
"(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
args: [{ to: "aaaaa-aa", amount: "100" }], // Strings!
})

Dynamic query with TanStack Query caching.

const balance = await reactor.fetchQueryDynamic({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
args: [{ owner: "aaaaa-aa" }],
})
// Subsequent calls return cached result

import { CandidDisplayReactor } from "@ic-reactor/candid"
async function getTokenBalance(canisterId: string, owner: string) {
const reactor = new CandidDisplayReactor({
name: "token-checker",
canisterId,
clientManager,
})
await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})
// Pass Principal as string, get balance as string
const balance = await reactor.callMethod({
functionName: "icrc1_balance_of" as any,
args: [{ owner }], // owner is a string like "aaaaa-aa"
})
return balance // "1000000" - ready for UI display!
}
import { CandidDisplayReactor } from "@ic-reactor/candid"
const reactor = new CandidDisplayReactor({
name: "transfer-actor",
canisterId,
clientManager,
candid: `service : {
transfer : (record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text });
}`,
})
await reactor.initialize()
// Add form validation
reactor.registerValidator("transfer", ([input]) => {
if (!input.to || input.to.length < 5) {
return {
success: false,
issues: [{ path: ["to"], message: "Invalid principal" }],
}
}
if (!input.amount || parseInt(input.amount) <= 0) {
return {
success: false,
issues: [{ path: ["amount"], message: "Amount must be positive" }],
}
}
return { success: true }
})
// Use in form submission
async function handleSubmit(formData: { to: string; amount: string }) {
try {
const result = await reactor.callMethod({
functionName: "transfer",
args: [formData], // Direct from form - no conversion needed!
})
console.log("Success:", result)
} catch (err) {
if (err instanceof ValidationError) {
// Show form errors
err.issues.forEach((issue) => {
console.log(`${issue.path}: ${issue.message}`)
})
}
}
}
import { useQuery, useMutation } from "@tanstack/react-query"
import { CandidDisplayReactor } from "@ic-reactor/candid"
// Create a reusable hook for dynamic canister queries
function useDynamicBalance(reactor: CandidDisplayReactor, owner: string) {
return useQuery({
queryKey: [reactor.canisterId.toString(), "icrc1_balance_of", owner],
queryFn: async () => {
await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})
return reactor.callMethod({
functionName: "icrc1_balance_of" as any,
args: [{ owner }], // String input
})
},
})
}
function BalanceDisplay({ canisterId, owner }: Props) {
const reactor = useMemo(
() =>
new CandidDisplayReactor({
name: `balance-${canisterId}`,
canisterId,
clientManager,
}),
[canisterId]
)
const { data: balance, isLoading } = useDynamicBalance(reactor, owner)
if (isLoading) return <div>Loading...</div>
// balance is already a string - no formatting needed!
return <div>Balance: {balance}</div>
}

Use Case Recommended Class
Dynamic form generation MetadataDisplayReactor
Dynamic result rendering with types MetadataDisplayReactor
Dynamic canister, UI display CandidDisplayReactor
Dynamic canister, raw types CandidReactor
Known canister, UI display DisplayReactor
Known canister, raw types Reactor