ic-reactor 4 beta.2 replaces generated hooks with client options
ic-reactor 4 beta.2 brings a new TypeScript and React client whose options work with TanStack Query’s hooks. Install beta-tagged packages to try it; 3.x remains the default until GA.

ic-reactor 4.0.0-beta.2 was published to npm under the beta dist-tag on October 6, 2026, as the second prerelease of ic-reactor 4. React and TypeScript developers calling Internet Computer canisters should review the new client model and try it in a beta branch before planning a migration.
ic-reactor is developed by B3Pay, which also publishes this blog.
What changed in ic-reactor 4
ic-reactor 4 is a breaking rewrite of the library for TypeScript and React applications. Instead of generating React hooks, developers generate a TypeScript module from each canister’s .did file with candid-core-cli gen, then bind its actor type to a client. The client builds options for TanStack Query’s own useQuery, useMutation and suspense hooks. The release puts three packages at the same version: @ic-reactor/core, @ic-reactor/react and @ic-reactor/vite-plugin.
An app creates a client with a network and identity or auth provider, then calls client.canister<Actor>(actor, { id }) or resolves a canister by name. The returned canister has plain async methods. For React reads and writes, client.queryOptions(canister, method, args) and client.mutationOptions(canister, method) feed the standard TanStack hooks. A Candid result variant with Ok or Err resolves to the Ok value or rejects with the Err value.
The cache key includes the network, caller principal, canister ID, method and arguments. That caller-scoped key prevents one principal’s cached response from being shown to another. Each principal also has an immutable agent. If a read started under one account settles after sign-in, sign-out or an account switch, it rejects as cancelled rather than populating the new caller’s cache.
Write failures carry a ReactorError with a mayHaveExecuted flag. It tells the app whether a failed write may still have changed canister state. The client re-sends an update only when it has proof that the canister never received it, such as reject code 2 or HTTP 429, at most twice within the call. TanStack Query itself does not retry mutations. Reads retry up to three times only for not_delivered, and never on a server. A write is refused before sending if nobody is signed in; a successful write invalidates reads for that canister by default.
Beta.2 adds the canister_not_found error code for calls routed to a missing canister, lets createTestClient().refuseNext() target a method or canister, and improves types so eligible queryOptions can be passed to suspense hooks without a cast. It also pins @candid-core/schema at 0.3.0 and @candid-core/cli at 0.2.0. The release notes say beta.1-generated modules need no regeneration for those pins. See the release and v4 changelog for the precise type-level changes.
Who is affected and why it matters
This release is for teams starting a new integration or evaluating a migration. TanStack Query owns the hook behavior, while ic-reactor supplies canister-aware options, caller-scoped caching and consistent errors. Apps can use the same read and mutation patterns as other TanStack Query data, while still handling the different risks of canister updates. For example, if a transfer error says it may have executed, an app can re-read the balance before offering another attempt; if the flag is false, the transfer had no effect.
Existing 3.x applications do not upgrade in place. The 4.0 API removes reactors, hook factories and generated hooks, and changes the Candid value types to follow candid-core: principals are branded text, optional values use T | null, variants use { tag, value }, and blobs are Uint8Array. The migration guide lists removed 3.x names and their replacements. The latest npm tag remains on 3.x, currently 3.13.0, until 4.0 GA; no GA date has been announced. Version 3.x receives security fixes only until then, and its documentation remains under /v3/.
Four example apps on the v4 branch cover a mainnet ICP ledger and fault-injecting sandbox, a React wallet on a local network, server rendering with one client per request, and a command-line agent tool. The examples and the v4 docs give developers starting points for browser, server and test setups.
What to do now
To evaluate beta.2, install the beta packages and the exact stable candid-core pair. These commands apply to ic-reactor 4.0.0-beta.2:
npm install @ic-reactor/core@beta @ic-reactor/react@beta @icp-sdk/core @tanstack/react-query
npm install --save-exact @candid-core/schema@0.3.0
npm install --save-dev --save-exact @candid-core/cli@0.2.0
npm install --save-dev @ic-reactor/vite-plugin@beta
Then generate each canister’s module from its .did file and port calls to the client. A balance read can use TanStack Query directly:
import type { Principal } from "@candid-core/schema"
import { formatUnits } from "@ic-reactor/core"
import { useClient } from "@ic-reactor/react"
import { skipToken, useQuery } from "@tanstack/react-query"
import { actor, type Actor } from "./generated/icrc1"
export function Balance({ owner }: { owner: Principal | undefined }) {
const client = useClient()
const ledger = client.canister<Actor>(actor, { id: "ryjl3-tyaaa-aaaaa-aaaba-cai" })
const balance = useQuery(
client.queryOptions(ledger, "icrc1_balance_of", owner ? { owner, subaccount: null } : skipToken)
)
return <p>{balance.data === undefined ? "…" : formatUnits(balance.data, 8)}</p>
}
A transfer uses mutation options from the same client:
import { useClient } from "@ic-reactor/react"
import { useMutation } from "@tanstack/react-query"
import { actor, type Actor, type TransferArg } from "./generated/icrc1"
export function Send({ arg }: { arg: TransferArg }) {
const client = useClient()
const ledger = client.canister<Actor>(actor, { id: "ryjl3-tyaaa-aaaaa-aaaba-cai" })
const transfer = useMutation(client.mutationOptions(ledger, "icrc1_transfer"))
return <button disabled={transfer.isPending} onClick={() => transfer.mutate(arg)}>Send</button>
}
When the transfer fails, transfer.error is a ReactorError. If error.mayHaveExecuted is true, the transfer may have gone through, so re-read the balance before offering a retry. If it is false, the transfer certainly had no effect. For a 3.x app, stay on 3.x for now or use the migration table to scope a separate port; a plain install without a beta tag still selects 3.x.
Get the wire in your inbox
Every new signal, straight from the generator. No noise, unsubscribe anytime.


