Skip to content

CandidReactor

CandidReactor extends the core Reactor class to support dynamic canister interaction. It can fetch or parse Candid at runtime, register methods dynamically, and then use all standard Reactor methods.

The key insight is that CandidReactor injects methods into the service at runtime. After initialization (or method registration), all standard Reactor infrastructure works automatically:

  • callMethod() — Direct method calls
  • fetchQuery() — TanStack Query caching
  • getQueryOptions() — For React hooks
  • invalidateQueries() — Cache invalidation
const reactor = new CandidReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
})
await reactor.initialize() // Fetch IDL from network
// Now it works like a regular Reactor!
const name = await reactor.callMethod({ functionName: "icrc1_name" })
import { CandidReactor } from "@ic-reactor/candid"
new CandidReactor(config: CandidReactorParameters)
ParameterTypeRequiredDescription
canisterIdCanisterIdYesThe canister ID to interact with
clientManagerClientManagerYesClient manager from @ic-reactor/core
candidstringNoCandid service definition (avoids network fetch)
idlFactoryInterfaceFactoryNoIDL factory (if already available)
actorANoExisting actor instance
const reactor = new CandidReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
})
// Fetches Candid from canister metadata
await reactor.initialize()

Fetch or parse Candid and update the service definition.

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.

await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})

After registration, all standard Reactor methods work with this function name.

OptionTypeDescription
functionNamestringThe method name to register
candidstringCandid signature or full service def

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",
},
])

Check if a method is registered.

if (reactor.hasMethod("icrc1_balance_of")) {
// Method is available
}

Get all registered method names.

const methods = reactor.getMethodNames()
// ["icrc1_name", "icrc1_symbol", "icrc1_balance_of", ...]

For quick calls without explicit registration, use these convenience methods. They register the method (if not already registered) and call it in one step.

Perform a dynamic query call.

const balance = await reactor.queryDynamic<bigint>({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
args: [{ owner: Principal.fromText("...") }],
})

Perform a dynamic update call.

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

Perform a dynamic query with TanStack Query caching.

const cachedBalance = await reactor.fetchQueryDynamic({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
args: [{ owner }],
})

After initialization or method registration, all inherited Reactor methods work:

MethodDescription
callMethod()Direct canister call
fetchQuery()Fetch with TanStack Query caching
getQueryOptions()Get options for useQuery
getQueryData()Get cached data without fetching
invalidateQueries()Invalidate cached queries
generateQueryKey()Generate cache key for a method

See Reactor for full documentation.


PropertyTypeDescription
adapterCandidAdapterThe internal CandidAdapter

All inherited Reactor properties are also available (canisterId, agent, queryClient, etc.).


async function exploreCanister(canisterId: string) {
const reactor = new CandidReactor({ canisterId, clientManager })
await reactor.initialize()
console.log("Methods:", reactor.getMethodNames())
// Try calling a common method
if (reactor.hasMethod("icrc1_name")) {
const name = await reactor.callMethod({ functionName: "icrc1_name" })
console.log("Token name:", name)
}
}
async function getTokenBalance(canisterId: string, owner: Principal) {
const reactor = new CandidReactor({ canisterId, clientManager })
// Register just the method we need
await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})
// Use fetchQuery for caching
return reactor.fetchQuery({
functionName: "icrc1_balance_of" as any,
args: [{ owner }],
})
}
import { useQuery } from "@tanstack/react-query"
function useDynamicQuery(
reactor: CandidReactor,
functionName: string,
candid: string,
args?: unknown[]
) {
return useQuery({
queryKey: [reactor.canisterId.toString(), functionName, args],
queryFn: async () => {
await reactor.registerMethod({ functionName, candid })
return reactor.callMethod({ functionName: functionName as any, args })
},
})
}