
How to call a smart contract using wagmi's v3 useWriteContract hook
Interacting with smart contracts through your React based UI
Note: This article has been updated from the original version to reflect the breaking changes and new patterns introduced in wagmi v3. The core concepts remain the same, but the API syntax has been modernized to align with TanStack Query conventions and improve type safety.
How do you call a smart contract function that performs a change of state (write) on the blockchain? And how could we be sure that the write transaction went through correctly?
wagmi
If you have ever done Web3 frontend, then you must have heard about or used the wagmi library. Wagmi allows devs to make all kind of blockchain interactions on the frontend in a manner that feels very native to React, with a set of 40+ React hooks. From getting info about the connected wallet to smart contract interaction passing through fetching blockchain state data.
As you know, I'm a big fan of the 80/20 rule. For me that means that even if most of the functionality that wagmi has to offer is interesting, you will probably use around 20 percent of it on a regular basis. That leaves us with...8 hooks?
I'm not going to cajole this series of articles to fit that number but the plan is to talk about what I use most frequently at work. I'm also going to focus on doubts and questions that frequently come up so you can speed run your smart contract interaction tasks.
Talking about frequently asked questions, another of the reasons for this article is that the wagmi library continues to evolve with breaking changes. After the significant changes from v1 to v2, wagmi v3 introduces a more streamlined API that better aligns with TanStack Query patterns. The main changes in v3 include:
-
Hook renames:
useAccount->useConnection(better reflects the connection model) -
Mutation function standardization: hooks now return a mutation object with
.mutate()and.mutateAsync()methods instead of direct functions -
TypeScript requirement bumped to 5.7.3+
-
Connector dependencies are now optional peer dependencies (install only the connectors you use)
You can see the full v3 migration guide here
First, a bit of setup
I will assume that you are using a React-based framework (I'm using Next.js) with Typescript.
Note: If you plan to use wallet connectors (WalletConnect, Coinbase, MetaMask, etc.), you now need to install their peer dependencies manually in v3.
- Install wagmi v3 and dependencies
npm install wagmi@3 viem@2 @tanstack/react-query
-
Create a custom Provider. We need to set up two providers:
-
The
WagmiProviderprovides blockchain context to your app -
The
QueryClientProviderfrom TanStack Query manages data fetching and caching
-
Don't forget to add use client if you are using Next.js with the App Router.
"use client"
import { WagmiProvider, createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Create wagmi config
const config = createConfig({
chains: [mainnet, sepolia],
transports: {
// RPC URL for each chain
[mainnet.id]: http(
process.env.NEXT_PUBLIC_ETH_MAINNET_RPC,
),
[sepolia.id]: http(
process.env.NEXT_PUBLIC_ETH_SEPOLIA_RPC,
),
},
});
// Create TanStack Query client
const queryClient = new QueryClient();
export const Web3Provider = ({ children }: { children: React.ReactNode }) => {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
};
Finally, add your custom provider to your root layout.tsx file in your Next project.
"use client"
import "./globals.css";
import React from "react";
import { Web3Provider } from "./Web3Provider";
export default function RootLayout({children}: Readonly<{children:React.ReactNode;}>) {
return (
<html>
<body>
<Web3Provider>
{children}
</Web3Provider>
</body>
</html>
);
};
Wallet UI note for v3
wagmi v3 moved wallet connector SDKs into optional peer dependencies. That means wagmi no longer bundles wallet SDKs by default, and connector UI libraries (like Rainbowkit and Connectkit) need to catch up to the new model. Some popular kits still target wagmi v2, so it can feel like v3 has "no wallet support" until you wire connectors yourself.
Practical path right now:
-
install only the connector packages you need (injected, WalletConnect, Coinbase, etc.)
-
pass them into
createConfig -
or build a small custom connect button with wagmi hooks while the ecosystem updates
If you are using injected wallets only, you can ship a tiny custom button with useConnect, useConnection, and useDisconnect and skip third-party UI kits.
Call a smart contract
To call a smart contract using wagmi v3, you will use useWriteContract. The hook returns a mutation object following TanStack Query conventions.
const writeContract = useWriteContract()
Key changes in v3: The mutation object exposes several useful properties and methods:
-
writeContract.data- Returnsundefineduntil you call the contract. Once called, it contains the transaction hash, useful for feeding intouseWaitForTransactionReceipt. -
writeContract.mutate()- Function to execute the contract call. Pass contract interaction params as an object. Use this inonClickhandlers or custom functions. -
writeContract.mutateAsync()- Promise-based version if you need to await the result. -
writeContract.isPending,writeContract.isSuccess,writeContract.isError- Status flags for tracking the mutation state.
const handleMint = (address: string) => {
console.log('Minting...');
writeContract.mutate({
address: process.env.CONTRACT_ADDRESS as Address,
abi: erc20Abi,
functionName: "mint",
args: [address],
})
};
You can also manually change the config for this hook, instead of letting it pick up from the global config. If you do this, you probably will also want to use a hook like useSwitchChain to drive the user to connect to your target chain.
const config = createConfig({
chains: [arbitrum]
transports: {
[arbitrum.id]: http()
},
})
const writeContract = useWriteContract({config})
How to handle next steps
If you only want to check if the tx was sent successfully, you can use the mutation object's status flags (isPending, isSuccess, isError) or pass callbacks to the .mutate() function like onSuccess, onError, or onSettled.
But some times you need to use the tx confirmation, the function return values or even an event emitted by the contract.
This is when useWaitForTransactionReceipt and useWatchContractEvent come in handy. I talk about the latter one on the next article but lets see now how you would wait for the tx receipt event.
Wait for the tx receipt with wagmi
After calling a smart contract, you normally wait for some confirmation that your transaction went through and then you make some action.
This is a key element of blockchain UX, your users want to be sure that they did everything right and the action that they intended to make is completed.
This is when useWaitForTransactionReceipt comes into play. Remember the writeContract.data property? That contains the tx hash of our contract call, and that's what we use as input for this hook.
const writeContract = useWriteContract();
const { data:receipt, isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({hash: writeContract.data as `0x${string}`});
The receipt object is a complex one, with lots of data about your transaction. Info about gas usage, transaction type and a logs object which you can decode to get data about events emitted by the transaction.
The logs field includes a data and a topics field. They are hex-encoded, but you can transform them to a human readable format using viem's lower level decodeEventLogs
const decodedLogs:any = decodeEventLog({
abi: ERC20Abi,
data: logs[0].data,
topics: logs[0].topics,
});
Auto-refresh after confirmation
A simple pattern is to refetch any related reads once the transaction is confirmed. This keeps the UI in sync without manual refreshes.
const writeContract = useWriteContract();
const { data: receipt, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
hash: writeContract.data as `0x${string}`,
});
const { refetch: refetchBalance } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
});
useEffect(() => {
if (!isConfirmed || !receipt) return;
refetchBalance();
}, [isConfirmed, receipt, refetchBalance]);
Example repo
I set up a repo with examples for this series of articles on my Github. Feel free to explore it and run it locally!
References
More Articles


