# 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: "create_post",
  invalidateQueries: [{ functionName: "get_posts" }],
})
```

## Configuration

| Option              | Type                                                       | Description                                 |
| ------------------- | ---------------------------------------------------------- | ------------------------------------------- |
| `functionName`      | `string`                                                   | The canister method to call (Required)      |
| `callConfig`        | `CallConfig`                                               | IC call configuration                       |
| `invalidateQueries` | `InvalidationTarget[]`                                     | Queries to invalidate on success            |
| `onSuccess`         | `(data, variables, onMutateResult) => void`                | Called on success                           |
| `onError`           | `(error, variables, onMutateResult) => void`               | Called on any error (network or canister)   |
| `onCanisterError`   | `(error, variables) => void`                               | Called specifically on canister logic error |
| `onSettled`         | `(data, error, variables, onMutateResult) => void`         | Called after mutation                       |
| `onMutate`          | `(variables) => onMutateResult \| Promise<onMutateResult>` | 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
import { CanisterError, createMutation } from "@ic-reactor/react"
import { backend } from "./reactor"

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:

```tsx
// 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: "create_post",
  invalidateQueries: [{ functionName: "get_posts" }],
})

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()`:

```tsx
import { createMutation, createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"

const likePostMutation = createMutation(backend, {
  functionName: "like_post",
})

// A query factory takes the args tuple, as in `getPostQuery([postId])`
const getPostQuery = createQueryFactory(backend, { functionName: "get_post" })

// In your component
function LikeButton({ postId }: { postId: string }) {
  const { mutate } = likePostMutation.useMutation({
    // Cancel outgoing refetches, write the new value, and return the rollback.
    // `post` is typed from the query; likes is a nat, which a DisplayReactor
    // shows as a string.
    onMutate: ([postId]) =>
      getPostQuery([postId]).optimisticUpdate((post) => ({
        ...post,
        likes: String(BigInt(post.likes) + 1n),
        isLiked: true,
      })),
    // Rollback on error
    onError: (_error, _args, update) => update?.rollback(),
    // Refetch to ensure consistency: a failed call may still have run
    onSettled: (_data, _error, [postId]) => getPostQuery([postId]).invalidate(),
  })

  return <button onClick={() => mutate([postId])}>Like</button>
}
```

`optimisticUpdate` skips the update when nothing is cached for the query, and
`rollback()` restores the value it replaced with its original fetch time. A
factory-level `onMutate` works the same way, and `execute()` runs it too.

### Form with React Hook Form

Integrate with React Hook Form:

```tsx
import { useForm } from "react-hook-form"
import { createMutation, parseTokenAmount } from "@ic-reactor/react"
import { backend } from "./reactor"

interface TransferForm {
  recipient: string
  amount: string
}

const transferMutation = createMutation(backend, {
  functionName: "transfer",
  invalidateQueries: [{ functionName: "get_balance" }],
})

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) => {
    // A DisplayReactor takes the nat as text, in base units
    mutate([data.recipient, parseTokenAmount(data.amount, 8).toString()])
  }

  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:

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

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

// In an event handler or utility function
async function handleWithdraw(amount: string) {
  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
function WithdrawButton() {
  return <button onClick={() => handleWithdraw("100")}>Withdraw</button>
}
```

`execute()` runs the mutation in the QueryClient's `MutationCache`, as
`useMutation()` does, with the factory's options. The factory's chain runs in
the same order as on the hook path: `onMutate`, then on success the factory's
`invalidateQueries` and its `onSuccess`, or on failure its `onCanisterError`
(for canister `Err` variants) and its `onError`, and then `onSettled`. A failed
call then **rethrows** the error, so `await execute(...)` still rejects for the
caller. Hook-level callbacks are absent because there is no hook on this path.

Because the mutation is in the `MutationCache`, the rest of TanStack Query sees
it too: the `MutationCache`'s global `onError`, `onSuccess` and `onSettled` run
for it, `useIsMutating` and `useMutationState` include it, and the factory's
`retry`, `networkMode` and `scope`, and the QueryClient's mutation defaults,
apply to it. That includes a `mutations.retry` default: it now re-sends a
failed `execute()` call as it does a `useMutation()` one, and each retry of an
update method is a new call that the canister runs, so a lost response can
execute it twice. Set `retry` on the factory to decide per method, for
example `retry: false`, or `retry: reactorUpdateRetry`, which retries only a
`SysTransient` rejection, the one failure that proves the call never ran.
With the default `networkMode: "online"`, a call made while TanStack
Query considers the browser offline pauses, like a `useMutation()` call, until
the connection is back and a mounted QueryClient resumes it (any IC Reactor
hook, or a `QueryClientProvider`, mounts one). Set `networkMode: "always"` on
the factory for a local replica you can reach offline. The settled mutation
stays in the cache for its `gcTime`, which TanStack Query sets to 5 minutes in
a browser and to no limit on a server, so create the QueryClient per request on
a server, as for queries.

### Multiple Related Refetches

`invalidateQueries` is a **list**, never a function of the mutation arguments.
Supply it on the factory when the queries are known up front, or on
`useMutation()` when they depend on component state. The hook's list is
invalidated after the factory's. Each entry is one of:

| Entry                    | Invalidates                                                          |
| ------------------------ | -------------------------------------------------------------------- |
| `{ functionName }`       | Every query of that method of this reactor, whatever its args        |
| `{ functionName, args }` | That method's queries for those args, infinite lists aside           |
| A query object           | That query, from any `create*Query` (suspense and infinite included) |
| A query factory          | Every query it returns, whatever its args, from any `create*Factory` |
| A `QueryKey`             | The queries under that key                                           |
| `undefined`              | Nothing, so `[maybeQuery]` is safe                                   |

The method name and args of a `{ functionName, args? }` entry are type-checked
against the canister, and its key is built when the mutation succeeds, so it
follows a `setCanisterId`. It names queries of the canister the mutation was
sent to, which is the one in the mutation's `callConfig.canisterId` when it
sets one. An infinite list keys its pages apart from its args, so name one by
its query object or factory, or by the method alone. A query object or factory
is invalidated in its own reactor's QueryClient.

#### Option 1: Fixed Queries (known up front)

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

const createCommentMutation = createMutation(backend, {
  functionName: "create_comment",
  invalidateQueries: [
    { functionName: "get_user_activity" },
    { functionName: "get_comments" }, // every post's comments
  ],
})
```

#### Option 2: Per-Component Queries

When the queries depend on props, pass the query objects to `useMutation()`:

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

const getPost = createQueryFactory(backend, { functionName: "get_post" })
const getComments = createQueryFactory(backend, {
  functionName: "get_comments",
})

const createCommentMutation = createMutation(backend, {
  functionName: "create_comment",
})

// In your component
function CommentForm({ postId }: { postId: string }) {
  const { mutate } = createCommentMutation.useMutation({
    // Invalidate this post and its comments after the mutation
    invalidateQueries: [getPost([postId]), getComments([postId])],
  })

  // ...
}
```

### Refetching with Query Objects

Pass a query object or a query factory as it is:

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

const postsQuery = createQuery(backend, {
  functionName: "get_posts",
})

const createPostMutation = createMutation(backend, {
  functionName: "create_post",
  invalidateQueries: [postsQuery],
})
```

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

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

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

// Hook level - runs second
function SaveProfileButton() {
  const { mutate } = updateProfileMutation.useMutation({
    onSuccess: () => {
      // `toast` is your notification helper, such as sonner's
      toast.success("Profile updated!")
    },
  })

  return <button onClick={() => mutate([{ name: "Alice" }])}>Save</button>
}
```

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.

`execute()` runs the factory's `onMutate` as well, and hands its result to the
factory's callbacks of the same call, so a factory `onError` can roll back
there too. They get `undefined` when the factory has no `onMutate`, since there
is no hook result on that path, and `onError` and `onSettled` get `undefined`
when `onMutate` throws.

### With Loading State UI

Build comprehensive loading states:

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

const deletePostMutation = createMutation(backend, {
  functionName: "delete_post",
})

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
import { createMutation } from "@ic-reactor/react"
import { backend } from "./reactor"

// ✅ Good - explicitly invalidate related data
const mutation = createMutation(backend, {
  functionName: "create_post",
  invalidateQueries: [
    { functionName: "get_posts" },
    { functionName: "get_posts_count" },
  ],
})

// ❌ Bad - keys start with the canister ID, so this matches nothing
const matchesNothing = createMutation(backend, {
  functionName: "create_post",
  invalidateQueries: [["get_posts"]],
})

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

### 2. Handle Errors

Always provide user feedback on errors:

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

const mutation = createMutation(backend, { functionName: "create_post" })

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

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

### 3. Use Pending State

Disable buttons and show loading indicators:

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

const mutation = createMutation(backend, { functionName: "create_post" })

function SubmitButton() {
  const { isPending } = mutation.useMutation()

  return (
    <button disabled={isPending}>{isPending ? <Spinner /> : "Submit"}</button>
  )
}
```

---

## Notes

**Tip:** Use `invalidateQueries` for automatic cache invalidation. This is more
  reliable than manually calling `queryClient.invalidateQueries()`: it is
  awaited before `onSuccess`, and a method or query object cannot be misspelled
  into a key that matches nothing.

**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:** An optimistic update needs a rollback. Return a query object's
  `optimisticUpdate()` from `onMutate` and call `rollback()` on the result in
  `onError`.

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