Skip to content
IC Reactor

DisplayReactor

Defined in: core/src/display-reactor.ts:150

DisplayReactor provides automatic type transformations between Candid and display-friendly types, plus optional argument validation.

  • bigint → string (for JSON/UI display)
  • Principal → string (text representation)
  • [T] | [] → T | undefined (optional unwrapping; null is also accepted as none in arguments)
  • Blobs (vec nat8) → hex strings, at every size

Register validators to check arguments before canister calls. Validators receive display types (strings), making them perfect for form validation.

Use DisplayReactor for UI/forms where principals and numeric values should be string-friendly. Use Reactor when you need raw Candid types directly.

import { DisplayReactor } from "@ic-reactor/core"
const reactor = new DisplayReactor<_SERVICE>({
clientManager,
name: "backend",
canisterId: "...",
idlFactory,
})
// Optional: Add validation
reactor.registerValidator("transfer", ([input]) => {
if (!input.to) {
return {
success: false,
issues: [{ path: ["to"], message: "Recipient is required" }]
}
}
return { success: true }
})
// Call with display types
await reactor.callMethod({
functionName: "transfer",
args: [{ to: "aaaaa-aa", amount: "100" }], // strings!
})

A = BaseActor

The actor service type, in Candid types. The display types are derived from it. Without it (BaseActor), every method’s display-side arguments and results are typed unknown, where a raw Reactor gives any: pass the service type, or cast to the display shape you expect.

T extends TransformKey = "display"

new DisplayReactor<A, T>(config): DisplayReactor<A, T>

Defined in: core/src/display-reactor.ts:161

DisplayReactorParameters<A>

DisplayReactor<A, T>

Reactor.constructor

readonly transform: T

Defined in: core/src/display-reactor.ts:154

Reactor.transform


readonly _actor: A

Defined in: core/src/reactor.ts:162

Phantom type brand for inference - never assigned at runtime

Reactor._actor


clientManager: ClientManager

Defined in: core/src/reactor.ts:164

Reactor.clientManager


name: string

Defined in: core/src/reactor.ts:165

Reactor.name


canisterId: Principal

Defined in: core/src/reactor.ts:166

Reactor.canisterId


service: ServiceClass

Defined in: core/src/reactor.ts:167

Reactor.service


pollingOptions: PollingOptions

Defined in: core/src/reactor.ts:168

Reactor.pollingOptions

get queryClient(): QueryClient

Defined in: core/src/reactor.ts:993

Get the query client from clientManager. This is the recommended way to access the query client for direct queries.

QueryClient

Reactor.queryClient


get agent(): HttpAgent

Defined in: core/src/reactor.ts:1001

Get the agent from clientManager. This is the recommended way to access the agent for direct calls.

HttpAgent

Reactor.agent

getCodec<M>(methodName): ActorMethodCodecs<A, M> | null

Defined in: core/src/display-reactor.ts:225

Get a codec for a specific method. Returns the args and result codecs for bidirectional transformation.

M extends string

M

The name of the method

ActorMethodCodecs<A, M> | null

Object with args and result codecs, or null if not found


registerValidator<M>(methodName, validator): void

Defined in: core/src/display-reactor.ts:261

Register a validator for a specific method. Validators receive display types (strings for Principal/bigint).

M extends string

M

The name of the method to validate

DisplayValidator<A, M>

The validator function receiving display types

void

// input.to is string, input.amount is string
reactor.registerValidator("transfer", ([input]) => {
if (!/^\d+$/.test(input.amount)) {
return {
success: false,
issues: [{ path: ["amount"], message: "Must be a valid number" }]
}
}
return { success: true }
})

unregisterValidator<M>(methodName): void

Defined in: core/src/display-reactor.ts:271

Unregister a validator for a specific method.

M extends string

M

void


hasValidator<M>(methodName): boolean

Defined in: core/src/display-reactor.ts:278

Check if a method has a registered validator.

M extends string

M

boolean


validate<M>(methodName, args): Promise<ValidationResult>

Defined in: core/src/display-reactor.ts:312

Validate arguments without calling the canister. Arguments are in display format (strings for Principal/bigint). Useful for form validation before submission.

Async validators run here, including fromZodSchema(schema, { async: true }). A validator that throws or rejects makes this reject with a CallError whose cause is what it threw, as callMethod() and callMethodWithValidation() do; a ValidationError it throws is passed on as it is.

M extends string

M

The name of the method

ReactorArgs<A, M, T>

The display-type arguments to validate

Promise<ValidationResult>

ValidationResult indicating success or failure

// Validate form data before submission
const result = await reactor.validate("transfer", [{
to: formData.recipient, // string
amount: formData.amount, // string
}])
if (!result.success) {
result.issues.forEach(issue => {
form.setError(issue.path[0], issue.message)
})
}

callMethodWithValidation<M>(params): Promise<ReactorReturnOk<A, M, T>>

Defined in: core/src/display-reactor.ts:376

Call a method with async validation support. Use this instead of callMethod() when you have async validators, such as fromZodSchema(schema, { async: true }).

A failed validation rejects with a ValidationError, and a validator that throws or rejects with a CallError whose cause is what it threw.

M extends string

M

ReactorArgs<A, M, T>

CallConfig

Promise<ReactorReturnOk<A, M, T>>

// Async validator (e.g., check if address is blocked)
reactor.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 }
})
await reactor.callMethodWithValidation({
functionName: "transfer",
args: [{ to: "...", amount: "100" }],
})

setCanisterId(canisterId): void

Defined in: core/src/reactor.ts:255

Set the canister ID for this reactor. Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).

CanisterId

The new canister ID (as string or Principal)

void

// Switch to a different ledger canister
ledgerReactor.setCanisterId("ryjl3-tyaaa-aaaaa-aaaba-cai")
// Then use queries/mutations as normal
const { data } = icrc1NameQuery.useQuery()

Reactor.setCanisterId


setCanisterName(name): void

Defined in: core/src/reactor.ts:276

Set the canister name for this reactor. Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).

string

The new canister name

void

// Switch to a different ledger canister
ledgerReactor.setCanisterName("icrc1")
// Then use queries/mutations as normal
const { data } = icrc1NameQuery.useQuery()

Reactor.setCanisterName


forCanister(canisterId): this

Defined in: core/src/reactor.ts:329

A reactor of the same class for another canister of the same interface, such as another ICRC ledger. It shares this reactor’s ClientManager (so its agent, identity and QueryClient), name and polling options. It starts from the interface, transform and validators this reactor has when it is made. Only the canister differs.

The same canister id always gives the same reactor, so it is safe to call during render and as a dependency of useMemo. The siblings of a reactor share that memo: a.forCanister(x).forCanister(y) is a.forCanister(y). The result is a separate reactor even for this reactor’s own canister, and it keeps its canister when this one’s setCanisterId moves.

Use one sibling per canister instead of retargeting a shared reactor with setCanisterId. Their query keys start with their own canister, so several tokens’ queries live side by side in the cache, and no call or retry of one is sent to another. A mutation invalidates its own canister’s queries. Never setCanisterId a sibling: ask for the other canister’s instead.

A subclass whose constructor takes options of its own passes them on by overriding the protected siblingParameters(canisterId). Its constructor has to build the reactor for the canisterId it is given: one that sets a canister of its own makes this throw rather than hand out a reactor for the wrong canister.

CanisterId

The other canister, as text or a Principal.

this

const ledger = new DisplayReactor<Ledger>({
clientManager,
idlFactory,
name: "ledger",
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP
})
// ckBTC, through the same agent, cache and interface
const ckbtc = ledger.forCanister("mxzaz-hqaaa-aaaar-qaada-cai")
const symbol = await ckbtc.fetchQuery({ functionName: "icrc1_symbol" })
// In React: one set of hooks per token, memoized by its canister
const hooks = useMemo(
() => createActorHooks(ledger.forCanister(tokenId)),
[tokenId]
)

Reactor.forCanister


getServiceInterface(): ServiceClass

Defined in: core/src/reactor.ts:397

Get the service interface (IDL.ServiceClass) for this reactor. Useful for introspection and codec generation.

ServiceClass

The service interface

Reactor.getServiceInterface


isQueryMethod<M>(methodName): boolean

Defined in: core/src/reactor.ts:423

Check if a method is a query method (query or composite_query).

M extends string

M

boolean

Reactor.isQueryMethod


generateQueryKey<M>(params, callConfig?): readonly unknown[]

Defined in: core/src/reactor.ts:465

M extends string

ReactorQueryParams<A, M, T>

CallConfig

readonly unknown[]

Reactor.generateQueryKey


getQueryRetry<M>(functionName, queryKey): ((failureCount, error) => boolean) | undefined

Defined in: core/src/reactor.ts:590

The retry a TanStack query of functionName, cached under queryKey, runs with when the query sets no retry of its own.

For a query method it is undefined, and the QueryClient’s retry defaults apply as usual.

An update method runs on the canister again each time the query function runs, as a new call the IC cannot tell from a retry, so a retry after a lost response executed the update a second time. For one it is a function that retries only a failure proving the canister never ran the call, a SysTransient rejection (see isRetryableUpdateError), and only as often as the QueryClient’s retry default for queryKey would: its setQueryDefaults or defaultOptions.queries, or TanStack Query’s own three retries in a browser when neither sets one. With the QueryClient defineReactor creates, that is reactorUpdateRetry, and with a default of retry: false nothing is retried. A transport failure, a SysUnknown rejection and an HTTP error are not retried. Refetches on mount, window focus, reconnect and invalidation still run the method again.

getQueryOptions, fetchQuery and the query hooks and factories of @ic-reactor/react apply it, and a retry the query sets itself wins over it.

M extends string

M

The method the query calls.

readonly unknown[]

The query’s key, which selects its QueryClient defaults.

((failureCount, error) => boolean) | undefined

Reactor.getQueryRetry


getQueryOptions<M>(params): FetchQueryOptions<ReactorQueryData<ReactorReturnOk<A, M, T>>>

Defined in: core/src/reactor.ts:613

The key and function of a TanStack query of the method, for queryClient.fetchQuery, prefetchQuery or useQuery. For an update method they also hold the retry from getQueryRetry; spread a retry of your own after them to replace it.

M extends string

ReactorCallParams<A, M, T>

FetchQueryOptions<ReactorQueryData<ReactorReturnOk<A, M, T>>>

Reactor.getQueryOptions


invalidateQueries<M>(params?, callConfig?): Promise<void>

Defined in: core/src/reactor.ts:677

Invalidate cached queries for this canister. This will mark matching queries as stale and trigger a refetch for any active queries.

It returns TanStack Query’s promise, which resolves once the active queries it matched have refetched. A refetch that fails does not reject it. Await it where the next step should read the refetched data, such as in a mutation’s onSuccess; an arrow that returns it, like onSuccess: () => reactor.invalidateQueries(...), keeps the mutation pending until those refetches finish.

M extends string

Partial<ReactorQueryParams<A, M, T>>

Optional parameters to filter the invalidation. Without a functionName, every query of the canister is invalidated.

CallConfig

Optional call configuration. Its canisterId selects which canister’s queries are invalidated, as it does for the calls.

Promise<void>

// Invalidate all queries for this canister, and wait for the active
// ones to refetch
await reactor.invalidateQueries()
// Invalidate only 'getUser' queries
reactor.invalidateQueries({ functionName: 'getUser' })
// Invalidate 'getUser' query for specific user
reactor.invalidateQueries({ functionName: 'getUser', args: ['user-1'] })
// Invalidate all queries of a canister reached through an override
reactor.invalidateQueries(undefined, { canisterId: otherLedgerId })

Reactor.invalidateQueries


callMethod<M>(params): Promise<ReactorReturnOk<A, M, T>>

Defined in: core/src/reactor.ts:730

Call a canister method directly using agent.call() or agent.query(). This is the recommended approach for interacting with canisters.

M extends string

Omit<ReactorCallParams<A, M, T>, "queryKey">

Promise<ReactorReturnOk<A, M, T>>

// Query method
const result = await reactor.callMethod({
functionName: 'greet',
args: ['world'],
});
// Update method with options
const result = await reactor.callMethod({
functionName: 'transfer',
args: [{ to: principal, amount: 100n }],
callConfig: { effectiveCanisterId: principal },
});

Reactor.callMethod


fetchQuery<M>(params, options?): Promise<ReactorQueryData<ReactorReturnOk<A, M, T>>>

Defined in: core/src/reactor.ts:832

Returns the cached value for the call, or fetches and caches it when there is none. Like TanStack Query’s ensureQueryData, it is cache-first: a cached value is returned even when it is stale or was invalidated, and a staleTime in options has no effect. For a value the canister returns now, use callMethod, or queryClient.fetchQuery with the options from getQueryOptions inside ClientManager.fetchAcrossIdentitySwitch, which keeps a sign-in or sign-out from resolving it with the previous principal’s cached data.

A sign-in or sign-out while the fetch is in flight cancels it, so the previous identity’s answer is never cached. The fetch then runs again for the identity installed now and resolves with that answer, rather than rejecting with TanStack’s CancelledError; see ClientManager.fetchAcrossIdentitySwitch.

M extends string

ReactorCallParams<A, M, T>

Omit<FetchQueryOptions<ReactorQueryData<ReactorReturnOk<A, M, T>>, ReactorReturnErr<A, M, T>, ReactorQueryData<ReactorReturnOk<A, M, T>>, readonly unknown[], never>, "queryKey" | "queryFn">

Further TanStack Query options for the fetch, such as retry, networkMode or meta. The query key and function always come from params. The query factories pass their config’s options through here, so a subclass that overrides this method still sees their fetches. Without a retry here, an update method retries as getQueryRetry says.

Promise<ReactorQueryData<ReactorReturnOk<A, M, T>>>

Reactor.fetchQuery


getQueryData<M>(params, callConfig?): ReactorQueryData<ReactorReturnOk<A, M, T>> | undefined

Defined in: core/src/reactor.ts:870

Get the current data from the cache without fetching.

M extends string

ReactorQueryParams<A, M, T>

CallConfig

ReactorQueryData<ReactorReturnOk<A, M, T>> | undefined

Reactor.getQueryData


subnetId(): Promise<Principal>

Defined in: core/src/reactor.ts:973

Get the subnet ID for this canister.

Promise<Principal>

Reactor.subnetId


subnetState(options): Promise<ReadStateResponse>

Defined in: core/src/reactor.ts:980

Get the subnet state for this canister.

ReadStateOptions

Promise<ReadStateResponse>

Reactor.subnetState