Skip to content

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>
)
}
OptionTypeDescription
functionNamestringThe canister method to call
OptionTypeDefaultDescription
callConfigCallConfig-IC agent call configuration
invalidateQueriesQueryKey[]-Query keys to invalidate on success
retrynumber | boolean0Number of retry attempts
retryDelaynumber | function-Delay between retries
onMutatefunction-Called before mutation
onSuccessfunction-Called on successful mutation
onErrorfunction-Called on mutation error
onSettledfunction-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:

PropertyTypeDescription
mutatefunctionTrigger the mutation
mutateAsyncfunctionTrigger and return a promise
dataTData | undefinedLast successful result
errorError | nullError if mutation failed
status'idle' | 'pending' | 'error' | 'success'Mutation status
isIdlebooleanTrue if mutation hasn’t run
isPendingbooleanTrue if mutation is running
isErrorbooleanTrue if mutation errored
isSuccessbooleanTrue if mutation succeeded
resetfunctionReset mutation state
variablesArgsVariables 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>
}
import { CanisterError } from "@ic-reactor/core"
const { mutate } = useActorMutation({
functionName: "transfer",
onError: (error) => {
if (error instanceof CanisterError) {
// Handle specific canister errors
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)
}
} else {
toast.error("Network error: " + error.message)
}
},
})
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.