Skip to content

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()
const reactor = new CandidDisplayReactor({
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(config: CandidDisplayReactorParameters)
ParameterTypeRequiredDescription
canisterIdCanisterIdYesThe canister ID to interact with
clientManagerClientManagerYesClient manager from @ic-reactor/core
candidstringNoCandid service definition (avoids network fetch)
idlFactoryInterfaceFactoryNoIDL factory (if already available)
validatorsRecord<string, Validator>NoInitial validators for method arguments
const reactor = new CandidDisplayReactor({
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 TypeDisplay TypeExample
nat, intstring1000000n"1000000"
nat64, int64string1234567890n"1234567890"
principalstringPrincipal.fromText(...)"aaaaa-aa"
opt TT | null[value]value, []null
blob (small)stringHex-encoded string

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
}
MethodDescription
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({ 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({
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({ 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 CaseRecommended Class
Dynamic canister, raw typesCandidReactor
Dynamic canister, UI displayCandidDisplayReactor
Known canister, raw typesReactor
Known canister, UI displayDisplayReactor