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
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
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 arraymutation.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:
// 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)
})| Option | Type | Purpose |
|---|---|---|
transactionName | string | Human-readable label. Surfaces in onMutationUpdate and your UI. |
address | Address | Override the contract's deploy address. Required for template contracts. |
simulate | boolean | Preflight 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:
<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:
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.
{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:
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:
<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), notsend([a, b, c]).uint256values arebigint. Use1_000_000n, never plain numbers.mutation.isLoadingcovers both signing AND mining.mutation.isPendingis signing-only.- Template contracts require
addressin options, it's not optional. mutation.reset()brings the hook back to idle. Useful after an error to re-enable the button.
Related
- Provider setup,
simulateMutations,onMutationUpdatedeep dive. - Template contracts,
.at()for reads,addressfor writes. - Per-block reactivity,
watchBlocksinteraction with mutations.