# 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

```typescript
import { createMutation } from "@ic-reactor/react"
```

## Basic Usage

```typescript
import { createMutation } from "@ic-reactor/react"
import { backend } from "./reactor"

const createPostMutation = createMutation(backend, {
  functionName: "createPost",
  invalidateQueries: [backend.generateQueryKey({ functionName: "getPosts" })],
})
```

## 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

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`?

- **Specific Targeting**: `onError` catches _everything_ (network issues, timeouts, throw errors). `onCanisterError` _only_ 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.

```typescript
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

| Property      | Type                              | Description                                                |
| ------------- | --------------------------------- | ---------------------------------------------------------- |
| `useMutation` | `(options?) => UseMutationResult` | React hook for components                                  |
| `execute`     | `(args) => Promise<T>`            | Imperative call that runs the factory-level callback chain |

---

## Examples

### TanStack Router with Actions

Combine mutations with TanStack Router's action handlers:

```typescript
// routes/posts/new.tsx
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

Update UI immediately before the canister responds. The value `onMutate`
returns reaches the `onError` defined at the same level, so the snapshot and
the rollback stay together. This example keeps both in `useMutation()`:

```typescript
const likePostMutation = createMutation(backend, {
  functionName: "likePost",
})

// In your component
const { mutate } = likePostMutation.useMutation({
  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

Integrate with React Hook Form:

```typescript
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)

Execute mutations from utilities or event handlers:

```typescript
const withdrawMutation = createMutation(backend, {
  functionName: "withdraw",
})

// In an event handler or utility function
async 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>
```

`execute()` runs the same factory-level chain the hook path does. On success it
awaits the factory's `invalidateQueries` and then its `onSuccess`; on failure it
calls the factory's `onCanisterError` (for canister `Err` variants) followed by
its `onError`, and then **rethrows**, so `await execute(...)` still rejects for
the caller. Hook-level callbacks are absent because there is no hook on this
path, and `onMutate`/`onSettled` do not run either — they belong to TanStack
Query's mutation lifecycle, which only `useMutation()` has.

### Multiple Related Refetches

`invalidateQueries` is always an **array of query keys** — never a function of the mutation arguments. Supply it on the factory when the keys are known up front, or on `useMutation()` when they depend on component state.

#### Option 1: Static Keys (if args are known in advance)

```typescript
const createCommentMutation = createMutation(backend, {
  functionName: "createComment",
  invalidateQueries: [
    backend.generateQueryKey({ functionName: "getUserActivity" }),
  ],
})
```

#### Option 2: Dynamic Keys (Recommended)

To keep your code clean and reusable, define helper functions for your query keys:

```typescript
// Define reusable query key helpers
const 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 component
function 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

If you are using `createQuery` (or other query factories), you can use their `getQueryKey` method directly:

```typescript
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

Factory and hook callbacks **chain** — both run, factory first. This holds for
all of them: `onSuccess`, `onError`, `onCanisterError`, `onMutate`, and
`onSettled`. A hook-level callback never replaces the factory's, so factory
teardown, telemetry, or logging cannot silently vanish because a call site
passed its own handler:

```typescript
// Factory level - runs first
const updateProfileMutation = createMutation(backend, {
  functionName: "updateProfile",
  onSuccess: () => {
    analytics.track("profile_updated")
  },
})

// Hook level - runs second
const { mutate } = updateProfileMutation.useMutation({
  onSuccess: () => {
    toast.success("Profile updated!")
  },
})
```

Each level's callbacks receive the return value of that level's own `onMutate`.
`onSuccess` and `onError` get it as their third argument, and `onSettled` gets
it as its fourth, after `data`, `error` and `variables`. Factory callbacks get
the factory's result and hook callbacks get the hook's, so rollback state can
live at either level. A factory without its own `onMutate` hands its callbacks the
hook's result.

Factory callbacks need `@tanstack/react-query` 5.89 or later to get the
factory's result. On older versions they receive the hook's result instead.
`execute()` does not run `onMutate`, so factory callbacks called through it
receive `undefined` there.

### With Loading State UI

Build comprehensive loading states:

```typescript
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

### 1. Define Invalidate Queries

Always specify which queries should be invalidated:

```typescript
// ✅ Good - explicitly invalidate related data
const mutation = createMutation(backend, {
  functionName: "createPost",
  invalidateQueries: [
    backend.generateQueryKey({ functionName: "getPosts" }),
    backend.generateQueryKey({ functionName: "getPostCount" }),
  ],
})

// ❌ Bad - no invalidation, cache becomes stale
const mutation = createMutation(backend, {
  functionName: "createPost",
})
```

### 2. Handle Errors

Always provide user feedback on errors:

```typescript
const { mutate, error } = mutation.useMutation({
  onError: (error) => toast.error(error.message),
})

{error && <ErrorMessage error={error} />}
```

### 3. Use Pending State

Disable buttons and show loading indicators:

```typescript
<button disabled={isPending}>
  {isPending ? <Spinner /> : "Submit"}
</button>
```

---

## Notes

**Tip:** Use `invalidateQueries` for automatic cache invalidation. This is more
  reliable than manually calling `queryClient.invalidateQueries()`.

**Note:** The `execute()` method is useful for route loaders, scripts, and utility
  functions outside of React's lifecycle. It runs the factory's
  `invalidateQueries`, `onSuccess`, `onCanisterError`, and `onError` — the same
  chain as the hook path — and rethrows failures after the callbacks run.

**Caution:** Optimistic updates require careful rollback handling. Always implement
  `onError` to restore previous state.

## See Also

- [useActorMutation](https://ic-reactor.b3pay.net/v3/reference/createactorhooks/useactormutation) — Direct hook usage
- [Mutations Guide](https://ic-reactor.b3pay.net/v3/framework/mutations) — Mutation patterns
- [createQuery](https://ic-reactor.b3pay.net/v3/reference/factories/createquery) — Query factory