Membuat UI DApp real-time: useWatchContractEvent untuk mendengarkan event seperti Transfer, perbandingan poll dan subscription, refresh saldo setelah transaksi, serta optimistic updates agar antarmuka terasa instan

Setelah di episode 11 kalian bisa read dan write, pada episode ini kita membuat UI merespons perubahan on-chain secara real-time. DApp yang bagus tidak menunggu user menekan refresh — ia tahu ketika transaksi selesai, ketika saldo berubah, atau ketika ada transfer masuk.
Mengapa episode ini penting? Karena data on-chain terus bergerak: harga, saldo, dan state kontrak berubah di luar kontrol user. Memahami event dan subscription membuat kalian membangun UI yang "hidup" alih-alih snapshot statis.
Ketika kontrak memancarkan event (misalnya Transfer saat token berpindah), blockchain menyimpan log tersebut. Frontend bisa membaca log ini dengan dua strategi:
| Strategi | Cara kerja | Cocok untuk |
|---|---|---|
| Polling | Query berkala (setiap N detik) | Data sederhana, jumlah query kecil |
| Subscription | Listen real-time via websocket | Event aktif, UX real-time |
Hook ini mendengarkan event dari kontrak. Contoh memantau transfer masuk ke address user:
import { useWatchContractEvent, useAccount } from "wagmi"
import { erc20Abi } from "viem"
import { TOKEN_ADDRESSES } from "../contracts"
export function TransferWatcher() {
const { address } = useAccount()
useWatchContractEvent({
address: TOKEN_ADDRESSES[1],
abi: erc20Abi,
eventName: "Transfer",
args: { to: address },
onLogs(logs) {
console.log("Transfer masuk:", logs)
},
})
return <p>Memantau transfer masuk...</p>
}Poin penting:
args memfilter event — di sini hanya Transfer dengan to = address user.onLogs dipanggil setiap blok yang mengandung event yang cocok.http() wagmi akan otomatis fallback ke polling.Untuk memfilter event dengan benar, pastikan ABI mencantumkan deklarasi indexed parameter yang difilter. Contoh pada ABI ERC-20: parameter from dan to bersifat indexed, sehingga bisa dipakai sebagai filter; value tidak. Filter non-indexed tidak bisa dilakukan — ini batas protokol, bukan bug wagmi.
Saldo yang ditampilkan harus segar. Dua pendekatan yang sering digabung:
const { refetch: refetchBalance } = useReadContract({
address: TOKEN_ADDRESSES[1],
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
})
const { isConfirmed } = useWaitForTransactionReceipt({ hash })
useEffect(() => {
if (isConfirmed) refetchBalance()
}, [isConfirmed, refetchBalance])Selain refetch manual, react-query mengelola cache: panggil invalidateQueries agar query dengan key yang sama dibaca ulang otomatis. Ini pola yang lebih scalable untuk banyak data on-chain.
Optimistic update = UI langsung menampilkan hasil seolah transaksi sudah sukses, lalu "memutar balik" jika ternyata gagal. Efeknya: tombol terasa instan, bukan menunggu blok.
// Asumsi: saldo lama 100
setBalance(prev => prev - amount) // langsung tampil 100 - amount
// lalu saat receipt gagal / revert:
setBalance(prev => prev + amount) // kembalikanKapan memakai:
Pola umum di produksi: tampilkan state "pending" dengan data optimis, lalu sinkronkan dengan data on-chain begitu receipt terkonfirmasi — dan rollback jika error.
Tip
Kombinasi terbaik untuk UI real-time: event untuk trigger (ada yang berubah), invalidateQueries untuk refresh cache, dan optimistic update untuk ilusi kecepatan. Jangan pakai polling 1 detik untuk semuanya — itu membebani RPC dan tidak lebih baik daripada event.
Rangkuman yang sudah kalian pelajari di episode 12:
useWatchContractEvent + filter args untuk mendengarkan event spesifik.refetch atau invalidateQueries setelah transaksi konfirmasi.Inti yang harus dibawa pulang:
Di episode 13 selanjutnya, kita membahas storage: IPFS, Arweave, dan Filecoin — konsep CID dan pinning, metadata NFT di IPFS lewat Pinata, perbedaan permanen vs berbayar, serta strategi kapan data on-chain dan kapan off-chain. Sampai jumpa di episode 13!