Skip to content

Mutations

useMutation is DappQL's write hook. It wraps wagmi's useWriteContract with typed args from your generated contracts, optional pre-send simulation, gas estimation, transaction-receipt tracking, and a single global update callback for toasts / analytics / logs.

Basic usage

tsx
import { ToDo } from './src/contracts'
import { useMutation } from '@dappql/react'

export function AddTask() {
  const mutation = useMutation(ToDo.mutation.addItem, 'Add task')

  return (
    <button
      disabled={mutation.isLoading}
      onClick={() => mutation.send('Buy milk', 0n)}
    >
      {mutation.confirmation.isSuccess ? 'Added' : 'Add task'}
    </button>
  )
}

ToDo.mutation.addItem is the typed mutation config generated by the CLI. send(...args) takes spread args, not an array, the types mirror the contract function's inputs exactly.

Full return shape

ts
const mutation = useMutation(ToDo.mutation.addItem, 'Add task')

mutation.status            // 'idle' | 'pending' | 'success' | 'error'
mutation.isIdle            // true before send()
mutation.isPending         // true while awaiting signature
mutation.isSuccess         // true once the tx hash is returned
mutation.isError
mutation.error             // WriteContractErrorType | null
mutation.failureCount
mutation.failureReason
mutation.submittedAt       // Date.now() of the last send()

mutation.data              // tx hash ('0x...' | undefined)

mutation.confirmation      // wagmi's useWaitForTransactionReceipt result
mutation.confirmation.isLoading  // tx mined?
mutation.confirmation.isSuccess  // receipt confirmed
mutation.confirmation.data       // the receipt when confirmed

mutation.isLoading         // shorthand, pending OR confirmation.isLoading
mutation.simulate(...args) // manual eth_call preflight; returns { result, request }
mutation.estimate(...args) // gas estimate
mutation.reset()           // clear state, back to idle

mutation.send(...args)     // broadcast, spread args, never an array

mutation.isLoading is the one you usually bind to disabled state, it covers both the signing and mining windows.

Configuration options

The second argument to useMutation is either a string (transaction name) or an options object:

ts
// String → just a transaction name
useMutation(ToDo.mutation.addItem, 'Add task')

// Object → full control
useMutation(Token.mutation.transfer, {
  transactionName: 'Send USDC',
  address: specificTokenAddr,   // override the contract's deployAddress
  simulate: true,               // preflight before signing (per-call)
})
OptionTypePurpose
transactionNamestringHuman-readable label. Surfaces in onMutationUpdate and your UI.
addressAddressOverride the contract's deploy address. Required for template contracts.
simulatebooleanPreflight via eth_call before signing. Aborts on revert. Defaults to the provider's simulateMutations.

Preflight with simulate

Simulation catches reverts before the user signs. Turn it on either per-call (above) or globally:

tsx
<DappQLProvider simulateMutations>
  {children}
</DappQLProvider>

With simulation on, every send() runs eth_call first. If the call would revert, the wallet prompt is skipped and mutation.error is populated with the revert reason.

For ad-hoc preflight or gas estimation that doesn't broadcast, call the helpers directly:

ts
const sim = await mutation.simulate(recipient, 1000n)
// sim.request is ready to pass to walletClient.writeContract directly if needed

const gas = await mutation.estimate(recipient, 1000n)

Waiting for the receipt

mutation.confirmation is wagmi's useWaitForTransactionReceipt hook, keyed on mutation.data (the tx hash). It starts loading automatically as soon as the hash is available and resolves when the tx is mined.

tsx
{mutation.isPending && <p>Signing…</p>}
{mutation.isSuccess && mutation.confirmation.isLoading && <p>Mining…</p>}
{mutation.confirmation.isSuccess && <p>Confirmed ✓</p>}
{mutation.error && <p>Failed: {mutation.error.message}</p>}

Template contracts

Template contracts (deployed at many addresses, user wallets, ERC20s, vaults) need the address in the options:

tsx
import { UserWallet, ERC20 } from './src/contracts'

const deposit = useMutation(UserWallet.mutation.deposit, {
  transactionName: 'Deposit',
  address: walletAddress,   // the specific wallet instance
})
deposit.send(assetAddress, amount)

const approve = useMutation(ERC20.mutation.approve, { address: tokenAddress })
approve.send(spender, amount)

See Template contracts for the full pattern.

Global transaction UX

Every mutation in your app emits lifecycle events ('submitted' | 'signed' | 'success' | 'error') through a single provider-level callback:

tsx
<DappQLProvider
  onMutationUpdate={({ status, contractName, functionName, transactionName, txHash, error, args }) => {
    if (status === 'submitted') toast.info(`Submitting ${transactionName}…`)
    if (status === 'signed')    toast.info(`${transactionName} signed`, { txHash })
    if (status === 'success')   toast.success(`${transactionName} confirmed`)
    if (status === 'error')     toast.error(error?.message ?? 'Transaction failed')
  }}
>
  {children}
</DappQLProvider>

One place to wire toasts, analytics, Sentry breadcrumbs, Segment events. No per-component glue. See Global transaction UX for deeper patterns.

Gotchas

  • send(...) takes spread args, not an array. send(a, b, c), not send([a, b, c]).
  • uint256 values are bigint. Use 1_000_000n, never plain numbers.
  • mutation.isLoading covers both signing AND mining. mutation.isPending is signing-only.
  • Template contracts require address in options, it's not optional.
  • mutation.reset() brings the hook back to idle. Useful after an error to re-enable the button.