
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.
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 functionsTypeScript 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 appThe
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 refactored connectors into optional peer dependencies. This reduces bundle size and lets you control wallet SDK versions, but it also means many wallet UI libraries (like Rainbowkit and Connectkit) still targeting wagmi v2 need time to update. The result is that v3 can look "unsupported" until you manually install connectors and pass them into createConfig.
If you only need injected wallets (MetaMask, Coinbase Wallet extension, etc.), a small custom button built with wagmi hooks is enough to unblock you while the ecosystem catches up.
Read from a single smart contract
Lets start reading data from a smart contract. You can access data from any function marked as view on a smart contract, and you don't even need that your user's wallet is connected to your application.
Oftentimes you will use these functions to check token ownership or balance, this is how it looks like.
export default function RandomComponent() {
const { address, isConnected } = useConnection();
const {
data: balance,
isLoading: isLoadingBalance,
isError: errorBalance,
isSuccess: successBalance,
} = useReadContract({
// APE coin
address: "0x4d224452801ACEd8B2F0aebE155379bb5D594381",
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
chainId: 1,
})
//...Rest of the component ...
}
Take into account:
If you are using
balanceor whateverdatayou have directly in your component, be sure to handle theundefinedcase well, as on the first render it will almost 100% return undefined.wagmi uses Tanstack Query, so the retries are baked in. You don't need to add
balanceto auseEffectdependency array just to fetch the data.If your output is a number, it will probably be a BigInt, handle with
formatEtherto get something readable.
I added chainId because, in my case, my app is configured to be able to read from both Sepolia and ETH Mainnet. Normally, wagmi hooks will fetch the config that you set up in the Provider, but if you need to read data from a different chain in just one specific component, you can define this config and only apply it to that contract call in the hook.
Example on how to read from the USDT contract on Arbitrum, just for this component. We first define the custom config for the component.
import { http, createConfig } from "wagmi"
import { arbitrum } from "wagmi/chains"
export const config = createConfig({
chains: [arbitrum],
transports: {
[arbitrum.id]: http(),
},
})
Then we just use this config on the hook
export default function RandomComponent() {
const { address, isConnected } = useConnection();
const { data: balance } = useReadContract({
config,
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
chainId: 42161,
})
//...Rest of the component ...
}
What happens if I need to wait for another piece of data as input for the read function?
Remember, if the hook has a dependency on the args array, just handle the undefined case.
Auto-refresh after writes
If a read depends on a write you just performed, you can refetch once the write confirms. This keeps the read UI fresh without polling.
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]);
Read from multiple smart contracts
You can also use wagmi's hooks to perform batch reads of different contracts, multiple functions inside the same contract or even multiple reads of the same smart contract with different input parameters.
To achieve this, we will use the useReadContracts hook, it works almost the same, but it takes an array of contract configs under the contracts key
const { data: balanceTest } = useReadContracts({
contracts: [{
address: "0x4d224452801ACEd8B2F0aebE155379bb5D594381",
abi: erc20Abi,
functionName: "balanceOf",
args: [testAddress],
chainId: 1,
},
{
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
abi: erc20Abi,
functionName: "balanceOf",
args: [testAddress],
chainId: 1,
}],
})
In this case, the response will be an array of objects, "[{result: 0n, status: success}]"
Of course, if you hold the list of contracts in a separate variable, you can use map to iterate over the whole list. This often looks cleaner and the code for the component ends up less cluttered. In this case you need to do a bit more of explicit type casting.
const { data: balanceTest } = useReadContracts({
contracts: contractConfigs.map(contract => ({
address: contract.address as `0x${string}`,
abi: contract.abi as Abi,
functionName: contract.functionName,
args: contract.args,
chainId: contract.chainId,
})),
})
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
About the Author

Ignacio Pastor
Fullstack Web3 Developer focused on NFTs and blockchain gaming. I blend my passion for Solidity with a deep understanding of smart contract development. It's not just about writing code; it's about creating solutions that are innovative and reliable. My expertise extends across a spectrum of token standards, including ERC721, ERC1155, and the innovative ERC6551, unlocking new possibilities for token-gated communities, blockchain gaming, and NFT marketplaces
More Articles


