Sebagian besar kode frontend memperlakukan pekerjaan asinkron seperti satu bentuk yang sama. Anda menjalankan sebuah Promise, menunggu hingga selesai (resolve), memasukkan hasilnya ke dalam state lokal, dan membiarkan framework melakukan rekonsiliasi diff. Pola ini sangat menggoda karena berhasil di mana saja: panggilan REST, pengiriman formulir, atau pesan WebSocket. Semuanya berakhir di useEffect atau event handler yang sama, semuanya dialirkan melalui setState, dan semuanya tampak seperti perpipaan async yang identik. Keseragaman tersebut adalah sebuah jebakan. Dalam aplikasi nyata, tidak semua pekerjaan asinkron itu sama. Berpura-pura bahwa semuanya sama akan mengubah komponen UI Anda menjadi arsitek data yang tidak disengaja, yang hanya disatukan dengan hook useEffect dan harapan semata.

Operasi async sebenarnya terbagi menjadi tiga jenis yang berbeda. Masing-masing memiliki hubungan yang berbeda dengan waktu, caching, dan kepemilikan (ownership). Belajar membedakan ketiganya adalah kunci untuk menjaga frontend tetap cepat, benar, dan masuk akal.

Queries: Fakta dengan Alamat

Sebuah query bukan sekadar fetch. Ia adalah permintaan untuk sebuah fakta yang dapat diidentifikasi. Anda meminta /user/123, bukan "beberapa data pengguna." Perbedaan itu penting karena identitaslah yang memungkinkan caching terjadi. Jika dua komponen pada layar yang sama membutuhkan catatan pengguna yang sama, mereka harus berbagi satu jawaban yang sama. Ketika setiap komponen menyimpan salinan lokalnya sendiri di useState, Anda memecah kebenaran data Anda. Avatar di header dan nama di sidebar menjadi tidak sinkron karena diambil pada waktu yang berbeda, atau salah satunya gagal sementara yang lain berhasil.

Anggaplah query sebagai sebuah sumber daya (resource), bukan sebuah tindakan (action). Ia memiliki cache key, kebijakan kesegaran (freshness policy), dan siklus hidup yang lebih lama dari komponen tunggal mana pun. Lapisan query yang dibangun dengan baik memahami bahwa membaca /projects?page=2 berbeda dengan membaca /projects?page=3. Setiap URL dan set parameter membentuk sebuah alamat, dan data pada alamat tersebut bisa saja basi (stale), segar (fresh), atau hilang. UI tidak seharusnya mengelola pembukuan ini. UI seharusnya meminta user:123 ke lapisan data dan menerima sebuah snapshot. Apakah snapshot tersebut berasal dari server dua detik yang lalu atau dari cache dua milidetik yang lalu bukanlah urusan komponen tersebut.

Dampak praktisnya langsung terasa. Ketika Anda memperlakukan setiap pembacaan sebagai fetch imperatif di dalam komponen, Anda kehilangan deduplikasi. Anda kehilangan penyegaran latar belakang (background refresh). Anda kehilangan kemampuan untuk menampilkan data cache secara instan sambil memvalidasinya di latar belakang. Sebuah query layak memiliki tempat di luar pohon UI Anda.

Mutations: Mengubah Dunia

Jika query bertanya tentang dunia, mutasi mengubahnya. Permintaan jaringan yang sebenarnya—POST, PUT, atau DELETE—biasanya adalah bagian yang mudah. Bagian yang sulit adalah segala sesuatu yang terjadi setelah server mengatakan "OK."

Misalkan seorang pengguna memperbarui nama tampilan mereka. Mutasi itu sendiri adalah satu permintaan tunggal. Namun radius dampaknya ada di mana-mana. Halaman profil menyimpan nama lama. Bilah navigasi menampilkan nama lama. Riwayat komentar mungkin merujuk padanya. Jika kode mutasi Anda tidak melakukan apa pun selain mengubah flag isLoading lokal dan kemudian memperbarui satu bagian state, aplikasi Anda sekarang sedang membohongi dirinya sendiri. Beberapa bagian UI berpura-pura bahwa perubahan telah terjadi. Bagian lainnya tidak tahu sama sekali bahwa ada perubahan.

Sebuah mutasi harus menyatakan dampaknya pada graf data (data graph). Ia harus memberi tahu sistem query mana yang sekarang tidak valid, kunci cache mana yang perlu diambil ulang (refetched), dan hubungan mana yang telah bergeser. Ini secara fundamental berbeda dari sebuah query. Query bersifat read-only dan dapat dibagikan. Mutasi berfokus pada penulisan (write-focused) dan bersifat merusak (destructive) terhadap cache yang ada. Menyamaratakan keduanya ke dalam abstraksi yang sama berarti pengembang akhirnya memanggil refetch() secara manual di dalam komponen secara acak, atau lebih buruk lagi, menyebar hook useEffect di seluruh pohon komponen untuk "mensinkronkan" kembali state lokal agar selaras dengan server.

Model kepemilikannya juga berbeda. Query biasanya dimiliki oleh cache. Mutasi dimiliki oleh tindakan pengguna yang memicunya. Ia memiliki state pending, state error, dan berpotensi memiliki nilai optimis (optimistic value) yang perlu di-roll