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.