createMutation
createMutation creates a reusable mutation object with pre-configured callbacks and invalidation logic. Perfect for update/insert operations that need to invalidate related queries.
Import
Section titled “Import”import { createMutation } from "@ic-reactor/react"Basic Usage
Section titled “Basic Usage”import { createMutation } from "@ic-reactor/react"import { backend } from "./reactor"
const createPostMutation = createMutation(backend, { functionName: "createPost", invalidateQueries: [backend.generateQueryKey({ functionName: "getPosts" })],})Configuration
Section titled “Configuration”| Option | Type | Description |
|---|---|---|
functionName | string | The canister method to call (Required) |
callConfig | CallConfig | IC call configuration |
invalidateQueries | QueryKey[] | Queries to invalidate on success |
onSuccess | (data, variables, context) => void | Called on success |
onError | (error, variables, context) => void | Called on any error (network or canister) |
onCanisterError | (error, variables) => void | Called specifically on canister logic error |
onSettled | (data, error, variables, context) => void | Called after mutation |
onMutate | (variables) => Promise<context> | Called before mutation (optimistic updates) |
Handling Canister Errors
Section titled “Handling Canister Errors”The onCanisterError callback is designed to handle business logic errors returned by the canister (e.g., Result.Err), separate from network or system errors.
Why use onCanisterError?
Section titled “Why use onCanisterError?”- Specific Targeting:
onErrorcatches everything (network issues, timeouts, throw errors).onCanisterErroronly fires when the canister executes successfully but returns an error result (e.g.,InsufficientFunds). - Typed Errors: The error object passed to this callback is a typed
CanisterError, making it easier to check specific error variants.
const transferMutation = createMutation(backend, { functionName: "transfer", onCanisterError: (error, args) => { // 'error' is a CanisterError<T> console.log("Error Key:", error.code) // e.g., "InsufficientFunds" console.log("Error Data:", error.err) // The generic data associated with the error
if (error.code === "InsufficientFunds") { toast.error("You don't have enough tokens!") } else { toast.error(`Transfer failed: ${error.code}`) } }, // 'onError' will still be called for network errors OR canister errors onError: (error) => { if (error instanceof CanisterError) { // Already handled above return } toast.error("Network error: " + error.message) },})Return Value
Section titled “Return Value”| Property | Type | Description |
|---|---|---|
useMutation | (options?) => UseMutationResult | React hook for components |
execute | (args) => Promise<T> | Direct execution (for utilities) |
Examples
Section titled “Examples”TanStack Router with Actions
Section titled “TanStack Router with Actions”Combine mutations with TanStack Router’s action handlers:
import { createFileRoute } from "@tanstack/react-router"import { createMutation } from "@ic-reactor/react"import { backend } from "../../reactor"
const createPostMutation = createMutation(backend, { functionName: "createPost", invalidateQueries: [ backend.generateQueryKey({ functionName: "getPosts" }), ],})
export const Route = createFileRoute("/posts/new")({ component: NewPostPage,})
function NewPostPage() { const navigate = Route.useNavigate()
const { mutate, isPending, error } = createPostMutation.useMutation({ onSuccess: (newPost) => { // Navigate to the new post after creation navigate({ to: `/posts/${newPost.id}` }) }, })
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget)
mutate([{ title: formData.get("title") as string, content: formData.get("content") as string, }]) }
return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="Post title" required /> <textarea name="content" placeholder="Content" required />
{error && <p className="error">{error.message}</p>}
<button type="submit" disabled={isPending}> {isPending ? "Creating..." : "Create Post"} </button> </form> )}Optimistic Updates
Section titled “Optimistic Updates”Update UI immediately before the canister responds:
const likePostMutation = createMutation(backend, { functionName: "likePost", onMutate: async (args) => { const [postId] = args const queryKey = getPostQuery(postId).getQueryKey()
// Cancel outgoing refetches await queryClient.cancelQueries({ queryKey })
// Snapshot current value const previousPost = queryClient.getQueryData(queryKey)
// Optimistically update queryClient.setQueryData(queryKey, (old: Post) => ({ ...old, likes: old.likes + 1, isLiked: true, }))
// Return context with snapshot return { previousPost } }, onError: (err, args, context) => { // Rollback on error const [postId] = args if (context?.previousPost) { queryClient.setQueryData( getPostQuery(postId).getQueryKey(), context.previousPost ) } }, onSettled: (data, error, args) => { // Refetch to ensure consistency const [postId] = args queryClient.invalidateQueries({ queryKey: getPostQuery(postId).getQueryKey(), }) },})Form with React Hook Form
Section titled “Form with React Hook Form”Integrate with React Hook Form:
import { useForm } from "react-hook-form"
interface TransferForm { recipient: string amount: string}
const transferMutation = createMutation(backend, { functionName: "transfer", invalidateQueries: [ backend.generateQueryKey({ functionName: "getBalance" }), ],})
function TransferPage() { const { register, handleSubmit, reset, formState } = useForm<TransferForm>()
const { mutate, isPending } = transferMutation.useMutation({ onSuccess: () => { reset() toast.success("Transfer successful!") }, onError: (error) => { toast.error(`Transfer failed: ${error.message}`) }, })
const onSubmit = (data: TransferForm) => { mutate([data.recipient, data.amount]) }
return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("recipient", { required: true })} placeholder="Recipient" /> <input {...register("amount", { required: true })} placeholder="Amount" />
<button type="submit" disabled={isPending}> {isPending ? "Sending..." : "Send"} </button> </form> )}Direct Execution (Outside React)
Section titled “Direct Execution (Outside React)”Execute mutations from utilities or event handlers:
const withdrawMutation = createMutation(backend, { functionName: "withdraw",})
// In an event handler or utility functionasync function handleWithdraw(amount: bigint) { try { const result = await withdrawMutation.execute([amount]) console.log("Withdrawal successful:", result) return result } catch (error) { console.error("Withdrawal failed:", error) throw error }}
// Use in onClick<button onClick={() => handleWithdraw(BigInt(100))}> Withdraw</button>Multiple Related Refetches
Section titled “Multiple Related Refetches”You can provide a list of query keys OR a function that returns them based on the mutation arguments.
Option 1: Static Keys (if args are known in advance)
Section titled “Option 1: Static Keys (if args are known in advance)”const createCommentMutation = createMutation(backend, { functionName: "createComment", invalidateQueries: [ backend.generateQueryKey({ functionName: "getUserActivity" }), ],})Option 2: Dynamic Keys (Recommended)
Section titled “Option 2: Dynamic Keys (Recommended)”To keep your code clean and reusable, define helper functions for your query keys:
// Define reusable query key helpersconst getPostKey = (postId: string) => backend.generateQueryKey({ functionName: "getPost", args: [postId] })
const getCommentsKey = (postId: string) => backend.generateQueryKey({ functionName: "getComments", args: [postId] })
const createCommentMutation = createMutation(backend, { functionName: "createComment",})
// In your componentfunction CommentForm({ postId }) { const queryClient = useQueryClient()
const { mutate } = createCommentMutation.useMutation({ // Invalidate the post and comments after mutation invalidateQueries: [getPostKey(postId), getCommentsKey(postId)], })
// ...}Refetching with Query Factories
Section titled “Refetching with Query Factories”If you are using createQuery (or other query factories), you can use their getQueryKey method directly:
import { createQuery, createMutation } from "@ic-reactor/react"import { backend } from "./reactor"
const postsQuery = createQuery(backend, { functionName: "getPosts",})
const createPostMutation = createMutation(backend, { functionName: "createPost", invalidateQueries: [ // Use the factory's getQueryKey method postsQuery.getQueryKey(), ],})Callback Chaining
Section titled “Callback Chaining”Factory callbacks run first, then hook callbacks:
// Factory level - runs firstconst updateProfileMutation = createMutation(backend, { functionName: "updateProfile", onSuccess: () => { analytics.track("profile_updated") },})
// Hook level - runs secondconst { mutate } = updateProfileMutation.useMutation({ onSuccess: () => { toast.success("Profile updated!") },})With Loading State UI
Section titled “With Loading State UI”Build comprehensive loading states:
function DeleteButton({ postId }: { postId: string }) { const { mutate, isPending, isSuccess, error, reset } = deletePostMutation.useMutation()
if (isSuccess) { return <span className="success">Deleted!</span> }
return ( <> <button onClick={() => mutate([postId])} disabled={isPending} className={isPending ? "deleting" : ""} > {isPending ? "Deleting..." : "Delete"} </button>
{error && ( <div className="error"> <span>{error.message}</span> <button onClick={reset}>Dismiss</button> </div> )} </> )}Best Practices
Section titled “Best Practices”1. Define Invalidate Queries
Section titled “1. Define Invalidate Queries”Always specify which queries should be invalidated:
// ✅ Good - explicitly invalidate related dataconst mutation = createMutation(backend, { functionName: "createPost", invalidateQueries: [ backend.generateQueryKey({ functionName: "getPosts" }), backend.generateQueryKey({ functionName: "getPostCount" }), ],})
// ❌ Bad - no invalidation, cache becomes staleconst mutation = createMutation(backend, { functionName: "createPost",})2. Handle Errors
Section titled “2. Handle Errors”Always provide user feedback on errors:
const { mutate, error } = mutation.useMutation({ onError: (error) => toast.error(error.message),})
{error && <ErrorMessage error={error} />}3. Use Pending State
Section titled “3. Use Pending State”Disable buttons and show loading indicators:
<button disabled={isPending}> {isPending ? <Spinner /> : "Submit"}</button>See Also
Section titled “See Also”- useActorMutation — Direct hook usage
- Mutations Guide — Mutation patterns
- createQuery — Query factory