Menerapkan interaksi contract di DApp: useReadContract untuk membaca state tanpa gas, useWriteContract untuk mengirim transaksi, useWaitForTransactionReceipt untuk memantau konfirmasi, plus pola approve-then-swap dan estimasi gas

Setelah di episode 10 user berhasil connect, pada episode ini DApp kalian mulai bicara dengan kontrak — membaca state dan mengirim transaksi. Ini adalah momen "cukup teori, waktunya beraksi".
Mengapa episode ini penting? Karena inilah batas paling nyata antara aplikasi web biasa dan DApp: setiap aksi yang menulis state adalah transaksi berbayar gas yang butuh konfirmasi. Developer yang tidak paham alur ini akan membuat tombol yang "tidak bekerja" — padahal sebenarnya transaksinya sedang pending di mempool.
Hook read memanggil fungsi view/pure — tanpa gas, tanpa signature, tanpa mengubah state. Contoh membaca saldo dan total supply:
import { useReadContract } from "wagmi"
import { formatEther } from "viem"
import { erc20Abi } from "viem"
import { TOKEN_ADDRESSES } from "../contracts"
import { useAccount } from "wagmi"
export function TokenBalance() {
const { address } = useAccount()
const { data, isPending, isError, refetch } = useReadContract({
address: TOKEN_ADDRESSES[1],
abi: erc20Abi,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
})
if (!address) return <p>Connect wallet dulu</p>
if (isPending) return <p>Membaca saldo...</p>
if (isError) return <p>Gagal membaca saldo</p>
return (
<div>
<p>Saldo: {formatEther(data ?? 0n)}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
)
}Poin penting:
query.enabled mencegah pemanggilan sebelum ada address — read tanpa argumen valid akan error.data adalah bigint (satuannya wei) — selalu formatEther/formatUnits sebelum ditampilkan.refetch untuk refresh manual saat user menuntut data terbaru.Write mengirim transaksi. Alurnya tiga tahap: kirim → tunggu receipt → tangani hasil.
import { useState } from "react"
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi"
import { parseEther } from "viem"
import { erc20Abi } from "viem"
import { TOKEN_ADDRESSES } from "../contracts"
export function Transfer() {
const [to, setTo] = useState("")
const [amount, setAmount] = useState("")
const { writeContract, data: hash, isPending } = useWriteContract()
const { isConfirming, isConfirmed } = useWaitForTransactionReceipt({ hash })
return (
<div>
<input value={to} onChange={(e) => setTo(e.target.value)} placeholder="0x..." />
<input value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="0.01" />
<button
disabled={isPending}
onClick={() =>
writeContract({
address: TOKEN_ADDRESSES[1],
abi: erc20Abi,
functionName: "transfer",
args: [to as `0x${string}`, parseEther(amount)],
})
}
>
{isPending ? "Menunggu wallet..." : "Transfer"}
</button>
{hash && <p>Tx: {hash}</p>}
{isConfirming && <p>Menunggu konfirmasi...</p>}
{isConfirmed && <p>Transfer berhasil!</p>}
</div>
)
}Alur yang benar:
writeContract memicu prompt MetaMask; data berisi transaction hash.useWaitForTransactionReceipt({ hash }) memantau hash sampai terkonfirmasi.isPending (menunggu tanda tangan) → isConfirming (di mempool) → isConfirmed.isPending di hook useWriteContract berbeda dari useWaitForTransactionReceipt — jangan tertukar.
Hampir semua interaksi token butuh dua langkah: user approve kontrak untuk menghabiskan token kalian, lalu kontrak transferFrom. Contoh untuk swap di DEX:
Frontend menangani ini sebagai dua transaksi berurutan — umumnya tombol kedua baru aktif setelah approve terkonfirmasi. Beberapa DApp memakai approve dengan maxUint256 agar tidak perlu approve ulang, dengan trade-off keamanan yang kita bahas di episode 16.
Sebelum transaksi, tampilkan estimasi gas agar user tidak kaget:
const gasEstimate = await publicClient.estimateContractGas({
address: tokenAddress,
abi: erc20Abi,
functionName: "transfer",
args: [to, parseEther(amount)],
account: address,
})Saat estimasi gagal dengan estimateGasExecutionError, biasanya ini tanda masalah nyata (saldonya kurang, allowance belum ada, atau argumen salah) — tangkap error ini untuk menampilkan pesan yang bisa dimengerti user.
Warning
Jangan pernah membiarkan error transaksi mentah tampil di UI ("Execution reverted: ..."). Wrap dalam blok try/catch dan terjemahkan ke bahasa manusia: saldo kurang, allowance belum approve, gas terlalu rendah, atau network sibuk. Error mentah adalah tanda DApp amatir.
Rangkuman yang sudah kalian pelajari di episode 11:
useReadContract — gratis, tanpa gas; Write = useWriteContract — berbayar, butuh signature.useWaitForTransactionReceipt → status confirmed.Inti yang harus dibawa pulang:
hash transaksi + status receipt — UI harus mengikuti keduanya.bigint di mana pun — format hanya saat ditampilkan.Di episode 12 selanjutnya, kita membahas events & real-time UI — useWatchContractEvent untuk mendengarkan event seperti Transfer, perbedaan poll dan subscription, refresh saldo setelah transaksi, serta optimistic updates agar UI terasa instan. Sampai jumpa di episode 12!