Skip to content
IC Reactor

Client

Defined in: core/src/client.ts:181

A client made by createClient: one per browser tab, or one per request on a server, so that no cache or agent is shared between users.

readonly network: string

Defined in: core/src/client.ts:183

The network’s segment in query keys: "ic", "local", a custom network’s name ?? host, or the resolved host for "env".


readonly queryClient: QueryClient

Defined in: core/src/client.ts:191

The QueryClient this client owns. Its default retry retries a read only after a failure that proves it was not delivered, and never on a server; its dehydrate and hydrate defaults keep bigint, Uint8Array and the floats JSON cannot write, so a server’s dehydrate() survives JSON.stringify and comes back exact.

caller(): string

Defined in: core/src/client.ts:193

The principal calls go out as now, as text: the anonymous principal unless signed in.

string


authState(): AuthState

Defined in: core/src/client.ts:198

Who calls go out as, and why. The object is the same until the status or the principal changes, so it can be read with useSyncExternalStore.

AuthState


subscribe(listener): () => void

Defined in: core/src/client.ts:205

Calls listener once after each change of Client.authState, and never for a notification of the auth that changed neither the status nor the principal (a renewed delegation, for one). Returns a function that stops listening. On a disposed client it adds nothing.

() => void

() => void


signIn(options?): Promise<void>

Defined in: core/src/client.ts:212

Signs in through the client’s auth, passing options on. Listeners are told once the auth settles, even if the auth itself did not notify. Rejects a TypeError on a client built with identity, which has no sign-in, and an Error on a server or after Client.dispose.

unknown

Promise<void>


signOut(options?): Promise<void>

Defined in: core/src/client.ts:217

Signs out through the client’s auth, passing options on (such as AuthClient’s { returnTo }). Rejects like Client.signIn.

unknown

Promise<void>


dispose(): void

Defined in: core/src/client.ts:231

Releases everything the client holds: it stops listening to its auth and disposes it, clears the QueryClient, and drops its agents. Calling it again does nothing.

Every call made on the client afterwards, a write included, rejects cancelled with code client_disposed and mayHaveExecuted: false, and sends nothing: a direct call, a query or mutation function, a func reference’s function. A call still waiting to be sent (for its caller’s identity, or to be sent again) is cancelled the same way. A direct call already sent settles as the replica answers; a read the client’s QueryClient was running is dropped with the cache.

void


canister<A>(service, target): Canister<A>

Defined in: core/src/client.ts:264

A canister to call: a frozen object with one plain async method per Candid method of service, typed from the generated Actor you name.

import { actor, type Actor } from "./canisters/icrc1"
const ledger = client.canister<Actor>(actor, { id: LEDGER_ID })
const balance = await ledger.icrc1_balance_of({ owner, subaccount: null })

service is the actor export of a module candid-core-cli gen wrote, or the actor of schemaFromContract(): both make the same calls and the same keys. <A> is not checked against service; pass the Actor of the same module.

A method sends its call as the caller signed in at the moment it is called, decodes the reply, and resolves with it as the Actor types it, except that a method whose one result is an Ok/Err variant resolves with the Ok payload and rejects canister_err with the Err one. An update or a oneway by a caller who is not signed in rejects unauthenticated and sends nothing. Every failure is a ReactorError; see Canister for what is re-sent.

The same service object and the same target give the same canister object on every call, so it can be made during render. See CanisterTarget for { id }, { name } and certified.

A

Schema<Principal>

CanisterTarget

Canister<A>

TypeError for a service that is not a service schema, or a target that is not one of the allowed shapes or whose id is not principal text.


queryKey<A, M>(canister, method?, vars?): readonly unknown[]

Defined in: core/src/client.ts:292

The query key of a read, or the prefix of a group of reads, for queryClient.invalidateQueries, getQueryData and filters:

  • queryKey(c): every read of the canister by the current caller.
  • queryKey(c, method): every read of that method by the current caller, whatever its arguments. For a method without arguments, which has one read, this is that read’s key, the same as queryKey(c, method, undefined), so queryClient.getQueryData(client.queryKey(ledger, "icrc1_fee")) finds what queryOptions(ledger, "icrc1_fee") cached.
  • queryKey(c, method, vars): the key queryOptions(c, method, vars) gives, as built now.

getQueryData and setQueryData match a key exactly: hand them a read’s whole key, never a prefix (a method’s with arguments, or a canister’s).

Keys are ['ic-reactor', network, caller, canisterId, method, args] plus 'certified' for a certified canister (DECISIONS Q5). Build them with this method, never by hand: a key typed out as an array literal is a QueryKey like any other, and matches nothing the client invalidates. It never throws for vars that do not encode: the key then holds '$invalid' and a text of the value.

A

M extends string

Canister<A>

M

VarsOf<A, M>

readonly unknown[]

TypeError for a canister of another client, or a method the service does not have.


queryOptions<A, M>(canister, method, …args): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>, never>

Defined in: core/src/client.ts:354

TanStack Query options for a read: useQuery(client.queryOptions(ledger, "icrc1_balance_of", account)), queryClient.fetchQuery(...), a QueryObserver.

The variables after method follow one rule (DECISIONS Q3): nothing for a method without arguments, the value for one argument, the tuple for two or more. Pass skipToken (from @tanstack/query-core or @tanstack/react-query) in their place while they are not known yet.

The read is made as the caller current when the options are built: the key holds their principal, and the query function refuses to run (cancelled, nothing sent) once someone else is signed in. Build the options again when the caller changes, as a component does on every render, and the read moves to the new caller’s key: one principal’s data is never shown under another’s.

Arguments that do not encode give a key tagged '$invalid' and a query function that rejects invalid_args with the codec’s issues, so a render never throws on half-typed input.

retry retries only a failure that proves the call was not delivered, at most 3 times, and never on a server.

No type says whether a method is a query or an update, so this checks at run time and throws a TypeError for an update or a oneway method: a query refetches, and every refetch would run the update again. Use Client.mutationOptions for a write. An update that returns the same answer however often it runs (ckBTC’s get_btc_address) can be read with { update: "idempotent" } as the fourth argument: it is fetched once per key and caller (staleTime: Infinity, no refetch on mount, focus or reconnect), needs a signed-in caller like any update, and is retried only after a failure that proves it never got in (DECISIONS Q2). It is still a read of its canister, so a write to that canister through Client.mutationOptions invalidates it by default and it runs once more, as a replicated call: safe for an idempotent method, but a cost. To keep it cached across writes, name the reads a write changes in invalidates.

A method without results (() -> () query) has nothing to cache: its call resolves undefined, which TanStack Query takes for a failed read. This throws a TypeError for it too; call it directly, as await canister.method().

Built from variables that cannot be skipToken, the options’ queryFn is never skipToken either, so useSuspenseQuery and useSuspenseQueries take them as they are. Variables of a type skipToken is assignable to, such as unknown (a Candid reserved argument) or {}, may be, so every read of such a method takes the next signature and keeps SkipToken.

A

M extends string

Canister<A>

M

…QueryArgs<A, M, never>

CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>, never>

TypeError for an update or oneway method without the opt-in, a method without results, a composite query of a certified canister, a canister of another client, a method the service does not have, or a fourth argument other than { update: "idempotent" }.

queryOptions<A, M>(canister, method, …args): CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>>

Defined in: core/src/client.ts:368

TanStack Query options for a read whose variables may be skipToken (V | SkipToken, or skipToken itself): the options’ queryFn is skipToken for a skipped read, which useQuery takes and useSuspenseQuery does not. Otherwise the same as the call with variables that cannot be skipped: see that signature for the rules.

A

M extends string

Canister<A>

M

…QueryArgs<A, M>

CanisterQueryOptions<DataOf<A, M>, ErrorOf<A, M>>

TypeError as the call with variables that cannot be skipped.


mutationOptions<A, M>(canister, method, options?): CanisterMutationOptions<VarsOf<A, M>, DataOf<A, M>, ErrorOf<A, M>>

Defined in: core/src/client.ts:418

TanStack Query options for a write: useMutation(client.mutationOptions( ledger, "icrc1_transfer")), then mutate(arg) with the same argument convention as Client.queryOptions.

The mutation function calls the method as the caller current when it runs, and an update by a caller who is not signed in rejects unauthenticated before anything is sent. retry is false: an update re-sends itself only when the failure proves the first attempt never got in (see Canister), and a TanStack retry would re-send after failures that do not prove it, running the write twice.

onSettled invalidates the reads the write may have changed, on this client’s queryClient, for every caller, certified or not, and waits for the active ones to refetch: after a success, a canister_err, and any failure whose mayHaveExecuted is true (a lost reply: the re-read shows whether it happened). Not after a failure that proves nothing ran (invalid_args, unauthenticated, not_delivered, a reject 1 or 3, a cancelled before sending). The reads are every read of the canister written to (DECISIONS Q10), or those listed in invalidates: canisters and [canister, method] pairs of this client; [] invalidates nothing. “Every read” includes an update read with { update: "idempotent" } (such as a ckBTC minter’s get_btc_address after update_balance), whose re-read is one more replicated call; list the reads in invalidates to leave it cached.

A { name } (the canister written to, or one in invalidates) is resolved once per run, in onMutate, which TanStack Query runs first and whose result it hands to onSettled: a write whose ic_env cookie entry changes before it settles (a local redeploy) invalidates the reads of the canister it wrote to. To add your own onSettled, call this one from it with every argument it gets; to add your own onMutate, call this one from it with both and spread its result into yours: { ...options.onMutate(variables, context), previous }. An onMutate that replaces this one keeps the write and its invalidation together from TanStack Query 5.89 on, which hands mutationFn and onSettled the same run context: mutationFn resolves the targets when it starts, and onSettled reads them from there. Before 5.89 it leaves mutationFn and onSettled each to resolve the targets when it runs, so a cookie rewritten between the two can split the write from its invalidation.

A

M extends string

Canister<A>

M

MutationOptionsOptions

CanisterMutationOptions<VarsOf<A, M>, DataOf<A, M>, ErrorOf<A, M>>

TypeError for a canister of another client, a method a service does not have, or a third argument other than { invalidates }.


func<F>(funcSchema, ref): F

Defined in: core/src/client.ts:446

An async function for a func reference a reply carried, such as an ICRC ledger’s archive callback:

import { QueryArchiveFn, type GetBlocksArgs, type BlockRange } from "./canisters/ledger"
for (const range of reply.archived_blocks) {
const read = client.func<(arg: GetBlocksArgs) => Promise<BlockRange>>(QueryArchiveFn, range.callback)
const { blocks } = await read({ start: range.start, length: range.length })
}

The call goes to ref.principal and ref.method, with the arguments, results and mode of funcSchema (the generated schema of the func type), through the same path as a canister’s methods: the same errors, the same caller rules, the same unwrapping. F is not checked against funcSchema: a generated func type carries no signature, so write the one its .did gives, with the Ok payload as the reply of a result.

F extends (…args) => Promise<unknown>

Schema<FuncValue>

FuncValue

F

TypeError for a schema that is not a func schema, or a ref whose principal is not principal text or whose method is not a name.