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"
const { useActorMutation } = createActorHooks(backend)
function CreatePost() {
const { mutate, isPending } = useActorMutation({
functionName: "createPost",
})
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 QueryKey[] - Query keys to invalidate on success
onCanisterError function - Called only for Result { Err } variants (canister logic errors)
retry number | boolean 0 Number of retry attempts
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
const { mutate, isPending, isSuccess } = useActorMutation({
functionName: "createPost",
})
const handleCreate = () => {
mutate([{ title: "Hello", content: "World" }])
}
const { mutate } = useActorMutation({
functionName: "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 { backend } from "../reactor"
const { mutate } = useActorMutation({
functionName: "createPost",
invalidateQueries: [
backend.generateQueryKey({ functionName: "getPosts" }),
backend.generateQueryKey({ functionName: "getPostCount" }),
],
})
const { mutateAsync, isPending } = useActorMutation({
functionName: "transfer",
})
const handleTransfer = async () => {
try {
const result = await mutateAsync([recipient, amount])
toast.success(`Transferred ${result.amount} successfully!`)
navigate("/transactions")
} catch (error) {
toast.error("Transfer failed")
}
}
function CreateUserForm() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const { mutate, isPending, isError, error, reset } = useActorMutation({
functionName: "createUser",
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 { useQueryClient } from "@tanstack/react-query"
import { backend } from "../reactor"
function LikeButton({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const queryKey = backend.generateQueryKey({
functionName: "getPost",
args: [postId],
})
const { mutate } = useActorMutation({
functionName: "likePost",
onMutate: async () => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey })
// Snapshot previous value
const previousPost = queryClient.getQueryData(queryKey)
// Optimistically update
queryClient.setQueryData(queryKey, (old: Post) => ({
...old,
likes: old.likes + 1,
isLiked: true,
}))
return { previousPost }
},
onError: (err, vars, context) => {
// Rollback on error
queryClient.setQueryData(queryKey, context?.previousPost)
},
onSettled: () => {
// Refetch to ensure consistency
queryClient.invalidateQueries({ queryKey })
},
})
return <button onClick={() => mutate([postId])}>❤️ Like</button>
}

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

const { mutate } = useActorMutation({
functionName: "transfer",
// Only fires for canister Result { Err } variants
onCanisterError: (error) => {
if ("InsufficientFunds" in error.err) {
toast.error("Not enough balance")
} else if ("InvalidRecipient" in error.err) {
toast.error("Invalid recipient address")
} 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:

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

Types are automatically inferred from your actor interface.