React 19 use() dan Actions: Model Async yang Baru

Foto oleh Christiaan Colen on flickr
Fungsi use membaca nilai resolve dari sebuah Promise atau nilai Context saat ini selama render. Jika diberikan Promise yang masih pending, komponen akan suspend sampai Promise itu resolve, dan Suspense boundary yang membungkusnya menampilkan fallback sementara menunggu. Berbeda dari hook lain, use bisa dipanggil secara kondisional atau di dalam loop, sehingga lebih fleksibel dibanding useContext untuk membaca Context.
Ini hampir selalu terjadi karena Promise baru dibuat pada setiap render, bukan di-cache. React mengharapkan instance Promise yang sama di setiap render ulang agar bisa melacak apakah Promise itu sudah resolve. Perbaiki dengan membuat Promise sekali saja di luar jalur render, misalnya di komponen induk, route loader, atau Map berdasarkan parameter request, lalu turunkan instance yang sama sebagai prop.
useActionState membungkus fungsi Action async dan secara otomatis melacak hasil, error, serta flag pending-nya, menggantikan apa yang sebelumnya membutuhkan beberapa useState terpisah ditambah logika try catch manual. Hook ini juga terintegrasi langsung dengan prop action pada form, sehingga React mereset status pending setelah action selesai tanpa kode tambahan.
Bisa. useOptimistic bekerja dengan Action apa pun, tidak hanya pengiriman form, termasuk klik tombol yang dibungkus startTransition atau panggilan di dalam useActionState. Syaratnya adalah setter dipanggil di dalam sebuah Action, karena nilai optimistik hanya kembali otomatis setelah Action tersebut selesai atau melempar error.
Tidak, adopsi bisa dilakukan bertahap. Form dan tombol dengan boilerplate state pending dan error manual paling banyak adalah titik awal terbaik karena pengurangannya langsung terlihat dan perubahannya terisolasi. Actions dan use juga berpadu dengan library data yang sudah ada seperti React Query, karena use cukup membongkar Promise yang sudah dihasilkan library tersebut.

Foto oleh Christiaan Colen on flickr
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.
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.
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.
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.
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>
);
}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.
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 |
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.
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.