React 19 secara diam-diam menulis ulang aturan tentang bagaimana komponen berkomunikasi dengan jaringan. Selama bertahun-tahun, mengambil data dan mengirim form di React selalu memiliki ritual yang sama: satu useState untuk hasil, satu useState untuk status loading, satu useState untuk error, lalu useEffect atau handler klik untuk menyambungkan semuanya. React 19 memadatkan sebagian besar boilerplate itu menjadi dua gagasan, yaitu fungsi use dan Actions, ditambah beberapa hook kecil yang dibangun di sekitarnya.
Panduan ini membahas apa yang sebenarnya dilakukan use, bagaimana Actions mengubah penanganan form dan tombol, serta di mana posisi useActionState dan useOptimistic. Tujuannya bukan menghafal seluruh permukaan API, melainkan memahami model mentalnya dengan cukup baik sehingga Anda tahu alat mana yang harus dipakai saat membangun fitur berbasis data berikutnya, baik itu form profil, kotak komentar, maupun daftar berpaginasi.
Apa yang Sebenarnya Dilakukan use()
Meskipun namanya terdengar seperti Hook, use bukanlah Hook dalam arti ketat, dan justru itulah yang membuatnya kuat. Fungsi ini bisa dipanggil di dalam kondisional, loop, dan setelah early return, sesuatu yang biasanya dilarang oleh Rules of Hooks. Apa yang dilakukan use mudah dijelaskan namun mudah pula disalahgunakan: berikan sebuah Promise atau objek Context, dan use mengembalikan nilai yang sudah resolve, sambil membuat komponen suspend selama Promise masih pending. Jika promise ditolak, error boundary terdekat akan menangkapnya, sehingga Anda tidak perlu lagi try dan catch di setiap fetch.
function Albums({ albumsPromise }) {
// suspends until albumsPromise resolves
const albums = use(albumsPromise);
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
);
}
function AlbumsPage({ albumsPromise }) {
return (
<Suspense fallback={<Spinner />}>
<Albums albumsPromise={albumsPromise} />
</Suspense>
);
}Jangan pernah membuat Promise yang diberikan ke use langsung di dalam body komponen pada setiap render. Promise baru di setiap render akan merusak Suspense dan bisa memicu render berulang tanpa henti. Buat Promise sekali saja, di komponen induk, route loader, atau cache kecil berdasarkan URL, lalu turunkan instance Promise yang sama sebagai prop.
Suspense, Bukan useEffect, yang Mengelola Status Loading
Perubahan praktis dengan use terletak pada di mana status loading berada. Alih-alih komponen melacak isLoading miliknya sendiri, komponen induk yang memulai fetch dan menurunkan Promise, sementara Suspense boundary yang membungkus konsumer menampilkan fallback sampai data resolve. Ini mencerminkan cara framework server seperti Next.js melakukan streaming data, dan artinya model mental yang sama kini berlaku baik untuk client component yang membaca Context secara kondisional maupun untuk Server Component yang melakukan streaming Promise langsung ke client. Suspense boundary yang bertumpuk juga memungkinkan sebuah halaman di-streaming secara bertahap, menampilkan kerangka halaman segera sementara bagian yang lebih lambat, seperti daftar komentar atau panel rekomendasi, resolve secara independen tanpa menghambat tampilan lainnya.
Membaca Context Tanpa Batasan Lama
Karena use bisa berjalan di dalam statement if, Anda bisa membaca Context seperti tema atau locale hanya ketika cabang render tertentu memang membutuhkannya, bukan memanggil useContext tanpa kondisi di bagian atas fungsi dan berharap nilainya tidak diperlukan pada cabang lain. Ini perubahan kecil, namun menghilangkan sekumpulan solusi akal-akalan di mana developer dulu memecah satu komponen menjadi dua hanya untuk memenuhi Rules of Hooks lama terkait pembacaan Context secara kondisional.
Jika sebuah fetch tidak pernah berubah selama umur komponen, simpan Promise tersebut di sebuah Map berdasarkan parameter request. Satu baris caching itu biasanya menjadi pembeda antara use yang bekerja mulus dan use yang menyebabkan suspend berulang.
Actions Mengubah Fungsi Async Menjadi Primitif UI Kelas Utama
Sebuah Action di React 19 hanyalah fungsi async yang diberikan ke prop action pada form, prop formAction pada tombol, atau dibungkus dengan startTransition. Yang membuatnya istimewa adalah apa yang dilakukan React secara otomatis di sekitarnya: status pending dilacak tanpa useState tambahan, nilai form sebelumnya tetap terjaga jika action melempar error, dan error dapat ditangkap oleh error boundary. useActionState mengemas pola ini menjadi satu hook yang mengembalikan state saat ini, action yang sudah dibungkus untuk dipanggil, dan flag isPending, sehingga berperilaku seperti useReducer untuk efek samping yang dipicu oleh interaksi pengguna. Argumen permalink yang opsional juga penting untuk progressive enhancement: jika JavaScript belum selesai dimuat namun pengguna tetap mengirim form, browser masih dapat menavigasi ke URL tersebut sebagai submit form HTML biasa alih-alih gagal secara diam-diam.
async function updateNameAction(previousState, formData) {
const name = formData.get("name");
const error = await updateName(name);
if (error) {
return error;
}
redirect("/profile");
return null;
}
function EditProfile() {
const [error, submitAction, isPending] = useActionState(
updateNameAction,
null
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Update"}
</button>
{error && <p>{error}</p>}
</form>
);
}useOptimistic: Feedback Instan yang Kembali Otomatis Jika Gagal
UI optimistik dulunya membutuhkan pencatatan manual yang teliti: perbarui state lokal segera, simpan nilai sebelumnya, lalu kembalikan jika request gagal. useOptimistic memformalkan pola ini. Hook ini menerima nilai terkonfirmasi saat ini dan mengembalikan nilai optimistik beserta setter-nya. Panggil setter secara sinkron di dalam sebuah Action sebelum request yang di-await selesai, dan React langsung menampilkan nilai optimistik tersebut, lalu otomatis kembali ke state asli begitu Action selesai, baik berhasil maupun melempar error.
function ChangeName({ currentName, onUpdateName }) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async (formData) => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<input type="text" name="name" disabled={currentName !== optimisticName} />
</form>
);
}useOptimistic hanya kembali secara otomatis ketika Action yang memicunya benar-benar selesai atau melempar error. Jika Anda memanggil setter di luar sebuah Action, atau jika fungsi async di sekitarnya tidak pernah resolve karena promise yang tidak ditangani, nilai optimistik bisa terlihat tertahan di layar tanpa batas waktu.
Memilih Hook yang Tepat untuk Setiap Kebutuhan
Dengan empat API yang saling berkaitan kini tersedia, akan lebih membantu jika membandingkannya secara berdampingan daripada memilih dari memori. Tabel di bawah ini mencantumkan tujuan praktis masing-masing serta situasi ketika pilihan itu jelas lebih tepat dibanding alternatif lainnya. Perlu dicatat bahwa useFormStatus sengaja bukan pengganti useActionState; hook ini ada khusus agar tombol submit yang berada jauh di dalam tree komponen bisa membaca status pending dari form induknya tanpa status tersebut harus diturunkan sebagai prop.
| API | Tujuan | Gunakan saat |
|---|---|---|
| use() | Membongkar Promise atau membaca Context saat render, suspend sampai siap | Membaca data yang diturunkan sebagai Promise yang sudah di-cache, atau membaca Context secara kondisional |
| useActionState | Membungkus Action async dan melacak hasil, error, serta flag pending-nya | Menangani submit form atau klik tombol yang memanggil mutasi ke server |
| useOptimistic | Menampilkan update UI instan yang otomatis kembali jika Action gagal | Like, follow, pesan chat, atau interaksi apa pun di mana feedback instan lebih penting daripada kepastian |
| useFormStatus | Membaca status pending dari form induk terdekat dari komponen anak | Tombol submit atau spinner berada di komponen yang terpisah dari form itu sendiri |
Daftar Periksa Migrasi Praktis
Mengadopsi API ini paling baik dilakukan sebagai migrasi bertahap, bukan penulisan ulang total. Mulailah dari form dan tombol yang sudah memiliki state pending dan error manual paling banyak, karena di situlah pengurangan boilerplate paling terasa dan risikonya paling kecil. Tidak ada keharusan untuk menyentuh data fetching dan penanganan form dalam satu langkah yang sama; memperlakukannya sebagai dua migrasi terpisah membuat setiap pull request cukup kecil untuk direview dengan teliti dan bisa di-rollback secara independen jika terjadi regresi.
- Ganti useState isSubmitting manual beserta blok try catch di sekitar handler form dengan useActionState terlebih dahulu, karena ini perubahan yang paling aman dan paling terisolasi.
- Pindahkan panggilan fetch yang saat ini berada di dalam useEffect dengan flag loading menjadi pembacaan Promise yang di-cache melalui use, dibungkus dalam Suspense boundary dengan fallback yang sungguhan, bukan layar kosong.
- Tambahkan useOptimistic hanya pada interaksi di mana pengguna mengharapkan feedback instan, seperti menandai favorit atau mengirim komentar, bukan pada setiap mutasi di aplikasi.
- Bungkus setiap konsumer use() baru dengan error boundary, karena Promise yang ditolak sekarang akan muncul melalui boundary tersebut, bukan tertangkap diam-diam di dalam useEffect.
Tim yang lebih dulu memigrasikan halaman-halaman dengan banyak form melaporkan penurunan boilerplate paling besar, karena useActionState saja biasanya menghilangkan tiga panggilan useState terpisah per form.
Actions dan use juga berpadu baik dengan React Query atau SWR. Model ini tidak mengharuskan Anda meninggalkan data layer yang sudah ada; use dapat membongkar Promise yang sudah dihasilkan dan di-cache oleh library query tersebut.

