wagmi adalah library React hooks untuk Ethereum. Ia dibangun di atas viem (lesson ethers.js / viem) dan TanStack Query, jadi koneksi wallet, pembacaan contract dan pengiriman transaksi jadi state React yang otomatis ter-update: lengkap dengan status loading, error dan success. Bayangin petugas meja depan Toko Kaos Raka. Kamu cukup bilang "tampilkan POIN Dinda" atau "tukar 100 POIN". Petugas itu yang bertanya ke gudang (node), mengingat jawabannya supaya gak bolak-balik (cache), menyodorkan formulir ke dompet pelanggan untuk ditandatangani, lalu mengabari kalau transaksinya sudah masuk block.
🧪 Tebakan cepat: Dinda menekan "Tukar 100 POIN", wallet-nya menyetujui, dan receipt-nya
successdi block 6. Tapi tepat setelah itu halaman masih menulis "POIN kamu: 160". Kenapa: transaksinya gagal, wagmi salah hitung, atau jawaban lama masih tersimpan di cache? Tebak dulu, jawabannya ada di bagian "Yang terjadi waktu dijalankan".
Versi dan catatan untuk halaman ini
Lesson ini memakai wagmi 3.7.7 (major 3, terbaru per Oktober 2026) dengan viem 2.57.4 dan @tanstack/react-query 5. Di wagmi 3 beberapa nama berubah: useAccount sekarang useConnection, daftar wallet diambil dengan useConnectors(), dan fungsi kirim dari useWriteContract dan useConnect bernama mutate. Nama lama masih ada tapi ditandai deprecated. Website BelajarWeb.dev sendiri gak memasang wagmi, jadi kode di bawah dijalankan di project terpisah: React 19 dan wagmi 3.7.7 di dalam happy-dom, melawan node Hardhat lokal, dengan wallet tiruan. Semua keluaran di lesson ini dari sana. Lab di halaman ini menjalankan versi tulisan tangan dari alur yang sama.
Config: chain, connector, transport
npm install wagmi@3 viem@2 @tanstack/react-query
// config.js
import { createConfig, http } from 'wagmi'
import { hardhat, sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const config = createConfig({
chains: [hardhat, sepolia],
connectors: [injected()],
transports: {
[hardhat.id]: http(),
[sepolia.id]: http(),
},
})
chains: cuma chain lokal (31337) dan Sepolia (11155111). Mainnet sengaja gak dimasukkan, jadi aplikasi ini gak bisa menyentuh uang asli.connectors: cara terhubung ke wallet.injected()memakai wallet yang memasang provider EIP-1193 di halaman (lesson Wallet). Selain itu wagmi otomatis mencari wallet lewat EIP-6963: tiap wallet yang mengumumkan diri muncul sebagai connector sendiri, misalnya "MetaMask".transports: jalur baca untuk tiap chain.http()tanpa URL memakai RPC bawaan chain itu (http://127.0.0.1:8545untuk Hardhat).
// App.jsx
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './config'
import { PoinSaya } from './PoinSaya'
const queryClient = new QueryClient()
export function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<PoinSaya />
</QueryClientProvider>
</WagmiProvider>
)
}
Dua provider ini wajib membungkus semua komponen yang memakai hook wagmi. QueryClient adalah cache TanStack Query tempat semua hasil baca disimpan.
Komponen: baca POIN dan tukar
// PoinSaya.jsx
import { useEffect } from 'react'
import { useConnection, useConnectors, useConnect, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { formatUnits, parseAbi, parseUnits } from 'viem'
const POIN = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
const abi = parseAbi([
'function balanceOf(address account) view returns (uint256)',
'function redeem(uint256 amount)',
'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
])
export function PoinSaya() {
const { address, status } = useConnection()
const connectors = useConnectors()
const connect = useConnect()
const saldo = useReadContract({
address: POIN,
abi,
functionName: 'balanceOf',
args: address ? [address] : undefined,
query: { enabled: Boolean(address) },
})
const tukar = useWriteContract()
const receipt = useWaitForTransactionReceipt({ hash: tukar.data })
const { refetch } = saldo
useEffect(() => {
if (receipt.isSuccess) refetch()
}, [receipt.isSuccess, refetch])
if (status !== 'connected') {
return connectors.map((connector) => (
<button key={connector.uid} onClick={() => connect.mutate({ connector })}>
Connect {connector.name}
</button>
))
}
return (
<div>
<p>POIN kamu: {saldo.data === undefined ? '...' : formatUnits(saldo.data, 18)}</p>
<button
disabled={tukar.isPending || receipt.isLoading}
onClick={() => tukar.mutate({ address: POIN, abi, functionName: 'redeem', args: [parseUnits('100', 18)] })}
>
{tukar.isPending ? 'Konfirmasi di wallet...' : receipt.isLoading ? 'Menunggu block...' : 'Tukar 100 POIN'}
</button>
{receipt.isSuccess && <p>Berhasil di block {String(receipt.data.blockNumber)}</p>}
{tukar.error && <p>{tukar.error.shortMessage ?? tukar.error.message}</p>}
</div>
)
}
useConnection(): alamat dan status koneksi ('connected','connecting','disconnected'...).useReadContract:readContractviem yang dibungkususeQuery.query: { enabled }mencegahnya jalan sebelum ada alamat.useWriteContract:writeContractviem yang dibungkususeMutation.mutate(...)membuka popup wallet;tukar.dataadalah hash-nya.useWaitForTransactionReceipt({ hash }): menunggu receipt hash itu. Selamahashmasihundefined, hook ini diam.refetchsaatreceipt.isSuccess: meminta ulangbalanceOfsetelah transaksinya masuk block.
Yang terjadi waktu dijalankan
Ini isi halaman dari render sungguhan, PoinRaka versi ERC-20 di chain lokal (Dinda 160 POIN), langkah demi langkah:
| Langkah | Teks di halaman | Di balik layar |
|---|---|---|
| render pertama | Connect Injected Connect Dompet Tiruan |
injected() plus wallet yang ditemukan lewat EIP-6963 |
| klik Connect Dompet Tiruan | POIN kamu: ... |
status jadi 'connected'; query balanceOf pending dan fetching |
POIN kamu: 160 |
query success, eth_call dijawab node |
|
| klik Tukar | Konfirmasi di wallet... |
tukar.isPending: popup wallet terbuka |
| wallet setuju | Menunggu block... |
ada hash; receipt fetching |
POIN kamu: 160 Berhasil di block 6 |
receipt success, tapi cache masih berisi 160 |
|
POIN kamu: 60 |
refetch membaca ulang balanceOf |
Baris "Berhasil di block 6" dengan "POIN kamu: 160" itulah jawaban tebakannya: transaksinya berhasil, tapi useReadContract menampilkan jawaban lama dari cache sampai dibaca ulang. TanStack Query menyimpannya dengan kunci ini:
["readContract",{"address":"0x5FbDB2315678afecb367f032d93F642f64180aa3","functionName":"balanceOf","args":["0x70997970C51812dc3A010C7d01b50e0d17dc79C8"],"chainId":31337}]
Komponen lain yang meminta balanceOf Dinda dengan kunci sama langsung mendapat jawaban yang sama dari cache, dan kalau keduanya meminta bersamaan, cuma satu eth_call yang dikirim.
Loading, error dan success
| Keadaan | Ditulis dengan | Tampilkan |
|---|---|---|
| baca jalan | saldo.status === 'pending' atau saldo.fetchStatus === 'fetching' |
... |
| menunggu tanda tangan | tukar.isPending |
"Konfirmasi di wallet..." |
| menunggu block | receipt.isLoading |
"Menunggu block..." |
| berhasil | receipt.isSuccess |
nomor block, lalu baca ulang saldo |
| gagal | tukar.error |
error.shortMessage |
Dua error yang muncul di render tadi. Dinda menekan Tolak di wallet: User rejected the request. Dinda menekan Tukar lagi saat sisa POIN-nya 60, dan tukar.error.message berisi:
The contract function "redeem" reverted.
Error: ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)
(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 60000000000000000000, 100000000000000000000)
Baris pertamanya adalah shortMessage. Karena error ERC20InsufficientBalance ada di abi, viem bisa menamainya. Tombolnya juga di-disabled selama menunggu, supaya gak terkirim dua kali.
⚠️ Jebakan yang sering ketemu
- Lupa membaca ulang setelah receipt. Saldo di layar tetap 160 walau chain sudah 60. Panggil
refetch(atauqueryClient.invalidateQueries({ queryKey })) saatreceipt.isSuccess, cukup sekali. - Menganggap
tukar.isSuccessberarti berhasil di chain. Itu cuma berarti wallet sudah mengembalikan hash. Yang menentukan adalahuseWaitForTransactionReceipt. - Membaca sebelum terhubung. Tanpa
query: { enabled: Boolean(address) }, hook mencoba membaca denganargskosong. - Memakai nama wagmi 2 di project wagmi 3.
useAccount,connectdanwriteContractmasih jalan tapi deprecated; tutorial lama memakai nama itu.
TL;DR: wagmi 3 adalah React hooks di atas viem dan TanStack Query. Siapkan createConfig dengan chains: [hardhat, sepolia], connector injected() (wallet EIP-6963 ditemukan otomatis) dan transports, lalu bungkus aplikasi dengan WagmiProvider dan QueryClientProvider. useConnection memberi alamat dan status, useConnectors plus useConnect().mutate menghubungkan wallet, useReadContract membaca balanceOf lewat cache (kunci ['readContract', {...}]), useWriteContract().mutate mengirim redeem, dan useWaitForTransactionReceipt menunggu block. Setelah receipt success, panggil refetch, kalau gak, layar masih menulis 160. Tampilkan isPending, isLoading, isSuccess dan error.shortMessage supaya pengguna tahu apa yang sedang terjadi.