Skip to content
IC Reactor

useActorMutation

useActorMutation is a React hook for calling update methods on Internet Computer canisters. It wraps TanStack Query’s useMutation with canister-specific functionality.

import { createActorHooks } from "@ic-reactor/react"
import { backend } from "./reactor"
const { useActorMutation } = createActorHooks(backend)
import { useActorMutation } from "./reactor"
function CreatePost() {
const { mutate, isPending } = useActorMutation({
functionName: "create_post",
})
return (
<button
onClick={() => mutate([{ title: "Hello", content: "World" }])}
disabled={isPending}
>
{isPending ? "Creating..." : "Create Post"}
</button>
)
}

UseActorMutationParameters extends react-query’s UseMutationOptions, giving you access to all standard react-query mutation options plus our custom actor options.

Option Type Description
functionName string The canister method to call
Option Type Default Description
callConfig CallConfig - IC agent call configuration
invalidateQueries InvalidationTarget[] - Queries to invalidate on success, before onSuccess
onCanisterError function - Called only for Result { Err } variants (canister logic errors)
retry number | boolean | function 0 Number of retry attempts, or a predicate such as reactorUpdateRetry
retryDelay number | function - Delay between retries
onMutate function - Called before mutation
onSuccess function - Called on successful mutation
onError function - Called on ALL errors (canister, network, agent)
onSettled function - Called after success or error
onMutate: (variables: Args) => Promise<Context> | Context
onSuccess: (data: Data, variables: Args, context: Context) => Promise<void> | void
onError: (error: Error, variables: Args, context: Context) => Promise<void> | void
onSettled: (data: Data | undefined, error: Error | null, variables: Args, context: Context) => Promise<void> | void

Returns a TanStack Query mutation result object:

Property Type Description
mutate function Trigger the mutation
mutateAsync function Trigger and return a promise
data TData | undefined Last successful result
error Error | null Error if mutation failed
status 'idle' | 'pending' | 'error' | 'success' Mutation status
isIdle boolean True if mutation hasn’t run
isPending boolean True if mutation is running
isError boolean True if mutation errored
isSuccess boolean True if mutation succeeded
reset function Reset mutation state
variables Args Variables passed to mutate
import { useActorMutation } from "./reactor"
function CreatePostButton() {
const { mutate, isPending, isSuccess } = useActorMutation({
functionName: "create_post",
})
const handleCreate = () => {
mutate([{ title: "Hello", content: "World" }])
}
return (
<button onClick={handleCreate} disabled={isPending}>
{isSuccess ? "Created" : "Create post"}
</button>
)
}
import { useActorMutation } from "./reactor"
// `toast` is your notification helper, such as sonner's
function useTransfer() {
return useActorMutation({
functionName: "icrc1_transfer",
onMutate: (args) => {
console.log("Starting transfer:", args)
return { startTime: Date.now() }
},
onSuccess: (data, args, context) => {
console.log("Transfer completed in", Date.now() - context.startTime, "ms")
toast.success("Transfer successful!")
},
onError: (error, args, context) => {
console.error("Transfer failed:", error)
toast.error("Transfer failed: " + error.message)
},
onSettled: () => {
console.log("Transfer finished (success or error)")
},
})
}
import { useActorMutation } from "./reactor"
import { getPost } from "../queries" // createQueryFactory(backend, ...)
function useCreatePost() {
return useActorMutation({
functionName: "create_post",
invalidateQueries: [
{ functionName: "get_posts" }, // every get_posts query, whatever its args
{ functionName: "get_posts_count" },
getPost, // every query this factory returned
],
})
}

Each entry is a { functionName, args? } method of the reactor, type-checked against the canister, a query object or query factory, or a query key. The invalidation is awaited before onSuccess runs, so the mutation stays pending until the invalidated queries in use have refetched. undefined entries are skipped.

import { useActorMutation } from "./reactor"
// `toast` is your notification helper, such as sonner's
function TransferButton() {
const { mutateAsync, isPending } = useActorMutation({
functionName: "icrc1_transfer",
})
const handleTransfer = async () => {
try {
const blockIndex = await mutateAsync([
{ to: { owner: recipient }, amount },
])
toast.success(`Transferred in block ${blockIndex}!`)
navigate("/transactions")
} catch (error) {
toast.error("Transfer failed")
}
}
return (
<button onClick={handleTransfer} disabled={isPending}>
Send
</button>
)
}
import { useState } from "react"
import { useActorMutation } from "./reactor"
function CreateUserForm() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const { mutate, isPending, isError, error, reset } = useActorMutation({
functionName: "create_user",
onSuccess: () => {
setName("")
setEmail("")
},
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
mutate([{ name, email }])
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isPending}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isPending}
placeholder="Email"
/>
{isError && (
<div className="error">
{error.message}
<button type="button" onClick={reset}>
Dismiss
</button>
</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
</form>
)
}
import { createQueryFactory } from "@ic-reactor/react"
import { backend, useActorMutation } from "../reactor"
const getPost = createQueryFactory(backend, { functionName: "get_post" })
function LikeButton({ postId }: { postId: string }) {
const { mutate } = useActorMutation({
functionName: "like_post",
// Cancel outgoing refetches, write the new value, keep a rollback
onMutate: ([postId]) =>
getPost([postId]).optimisticUpdate((post) => ({
...post,
// A DisplayReactor caches a `nat` as text
likes: (BigInt(post.likes) + 1n).toString(),
isLiked: true,
})),
// Rollback on error
onError: (_error, _args, update) => update?.rollback(),
// Refetch to ensure consistency
onSettled: (_data, _error, [postId]) => getPost([postId]).invalidate(),
})
return <button onClick={() => mutate([postId])}>❤️ Like</button>
}

The updater is typed from the query and gets the value in the cache (before any select); it is not called when nothing is cached. See Optimistic Updates.

Use onCanisterError to handle Result { Err } variants separately from network or agent errors. This is cleaner than checking isCanisterError(error) inside onError:

import { useActorMutation } from "./reactor"
// `toast` is your notification helper, such as sonner's
function useTransfer() {
return useActorMutation({
functionName: "icrc1_transfer",
// Only fires for canister Result { Err } variants
onCanisterError: (error) => {
// A DisplayReactor gives a variant a `_type`; switch on it
if (error.err._type === "InsufficientFunds") {
toast.error("Not enough balance")
} else if (error.err._type === "BadFee") {
toast.error("Wrong transfer fee")
} else {
toast.error("Transaction failed: " + error.code)
}
},
// Fires for ALL errors: canister Err, network failures, agent errors
onError: (error) => {
console.error("Unexpected error", error)
},
})
}

onCanisterError also receives the mutation variables as its second argument:

import { useActorMutation } from "./reactor"
function useTransfer() {
return useActorMutation({
functionName: "icrc1_transfer",
onCanisterError: (error, [{ to, amount }]) => {
console.error(
`Transfer of ${amount} to ${to.owner} failed: ${error.code}`
)
},
})
}
import { useActorMutation } from "./reactor"
function PublishButton() {
const { mutate } = useActorMutation({
functionName: "create_post",
onSuccess: () => {
// This runs first (from hook config)
console.log("Post created!")
},
})
// In an event handler:
const handlePublish = () =>
mutate([postData], {
onSuccess: (post) => {
// This runs second (from mutate call)
navigate(`/posts/${post.id}`)
},
})
return <button onClick={handlePublish}>Publish</button>
}
import { useEffect } from "react"
import { useActorMutation } from "./reactor"
function TransferButton() {
const { mutate, isPending, isSuccess, reset } = useActorMutation({
functionName: "icrc1_transfer",
})
useEffect(() => {
if (isSuccess) {
const timer = setTimeout(reset, 3000)
return () => clearTimeout(timer)
}
}, [isSuccess, reset])
return (
<button
onClick={() => mutate([{ to: { owner: recipient }, amount }])}
disabled={isPending}
>
{isPending && "Transferring..."}
{isSuccess && "✓ Done!"}
{!isPending && !isSuccess && "Transfer"}
</button>
)
}
useActorMutation<M extends FunctionName<A>>(
options: UseActorMutationParameters<A, M, T>
)

Types are automatically inferred from your actor interface.