Nextjs Forms Server Actions Optimistic UI Pending States
Definisi
Di Next.js App Router, form tidak selalu harus dikelola penuh di client. Ada beberapa lapisan yang sering dipakai bersama:
- Client-managed form: state input, validasi ringan, dan interaksi UI dikelola di browser
- Server Action: fungsi server yang menerima submit form dan menjalankan perubahan data di server
- Optimistic UI: tampilan sementara yang langsung berubah sebelum response server selesai
- Pending state: indikator bahwa submit sedang diproses, supaya user tidak menebak-nebak status request
Topik ini penting karena form yang sehat bukan cuma soal “bisa submit”. Yang perlu dijaga juga adalah:
- kapan input memang butuh state di client
- kapan logic sebaiknya dipindah ke server
- bagaimana UI tetap terasa cepat saat server masih bekerja
- bagaimana error, rollback, dan revalidation disusun tanpa bikin state liar
Kapan form dikelola di client
Form dikelola di client kalau interaksinya bergantung pada state browser atau perlu respons instan sebelum submit.
Contoh situasi yang cocok:
- validasi per karakter atau per field
- show/hide password
- input yang saling memengaruhi, misalnya country → city
- draft lokal yang disimpan sementara
- form multi-step dengan navigasi lokal
- input yang perlu preview langsung, misalnya file upload preview atau markdown preview
Pola client-managed biasanya memakai useState, useReducer, atau library form seperti React Hook Form. Tujuannya bukan sekadar “lebih modern”, tapi karena browser memang tempat yang paling tepat untuk state interaktif yang bersifat lokal.
Tanda form sebaiknya tetap di client
- aturan validasi terlalu bergantung pada interaksi langsung user
- UI harus merespons tanpa round-trip server
- nilai form dipakai untuk kalkulasi visual di halaman yang sama
- error helper harus muncul saat user masih mengetik
Kalau semua hal dipaksa lewat server, form akan terasa berat dan patah-patah.
Kapan Server Action dipakai
Server Action dipakai saat submit form adalah operasi yang mengubah data di server.
Contoh yang cocok:
- create post
- update profil
- ubah status order
- simpan komentar
- menambah item ke cart yang tersimpan di backend
Server Action paling pas kalau form punya karakter berikut:
- data harus divalidasi dan disimpan di server
- hasil perubahan harus memengaruhi cache atau halaman lain
- operasi butuh akses ke database, secret, session, atau service internal
- logika submit ingin dekat dengan komponen yang memakainya tanpa bikin route handler terpisah
'use server'
import { revalidatePath } from 'next/cache'
export async function updateProfile(formData: FormData) {
const name = String(formData.get('name') ?? '')
if (!name.trim()) {
return { ok: false, message: 'Nama wajib diisi' }
}
// simpan ke database
revalidatePath('/profile')
return { ok: true }
}Lalu dipakai dari form:
<form action={updateProfile}>
<input name="name" />
<button type="submit">Simpan</button>
</form>Kenapa Server Action enak untuk form
- submit lebih dekat ke alur nyata: browser → server → update → revalidate
- secret dan akses data tetap aman di server
- komponen client tidak perlu memegang logic mutation yang sensitif
- revalidation bisa dilakukan di tempat yang sama dengan mutation
Kapan jangan memaksa Server Action
- form butuh validasi sangat interaktif sebelum submit
- user harus melihat hasil input instan saat mengetik
- operasi bukan mutation server, hanya transformasi lokal
- Anda butuh kontrol rendah-level atas request/response yang lebih cocok lewat route handler
Cara kerja optimistic UI
Optimistic UI adalah teknik saat UI diperbarui lebih dulu seolah-olah request berhasil, lalu disesuaikan lagi setelah response server datang.
Tujuannya sederhana: membuat aplikasi terasa cepat.
Contoh paling umum:
- user menambah komentar
- komentar langsung muncul di daftar
- server tetap memproses penyimpanan di belakang layar
- kalau gagal, komentar sementara dihapus atau ditandai error
Optimistic UI bukan berarti “asal tampil dulu”. Ia harus punya strategi rollback.
Alur dasar optimistic update
- user submit form
- UI menambahkan state sementara
- Server Action dipanggil
- server memvalidasi dan menyimpan data
- jika sukses, UI disesuaikan dengan data final
- jika gagal, optimistic state dibatalkan atau dikoreksi
- data yang ter-cache direvalidate agar sinkron
Contoh pola optimistic state
'use client'
import { useOptimistic, useTransition } from 'react'
type Comment = {
id: string
text: string
pending?: boolean
}
export function CommentForm({ comments, addComment }: {
comments: Comment[]
addComment: (formData: FormData) => Promise<void>
}) {
const [isPending, startTransition] = useTransition()
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, text: string) => [
...state,
{ id: crypto.randomUUID(), text, pending: true },
]
)
return (
<form action={(formData) => {
const text = String(formData.get('text') ?? '')
addOptimisticComment(text)
startTransition(() => addComment(formData))
}}>
<input name="text" />
<button type="submit" disabled={isPending}>Kirim</button>
<ul>
{optimisticComments.map((comment) => (
<li key={comment.id}>
{comment.text} {comment.pending ? '(mengirim...)' : ''}
</li>
))}
</ul>
</form>
)
}Catatan penting: contoh di atas memperlihatkan ide, bukan satu-satunya bentuk implementasi. Di proyek nyata, Anda sering perlu menata ulang bagaimana data final masuk lagi setelah action selesai.
Pending states
Pending state adalah sinyal UI bahwa proses submit sedang berjalan.
Bentuk yang umum:
- tombol submit menjadi disabled
- label tombol berubah menjadi “Menyimpan…”
- spinner atau progress ring muncul
- input tertentu dikunci sementara
- banner kecil menjelaskan bahwa request belum selesai
Pending state berbeda dari optimistic UI:
- pending state memberi tahu bahwa request masih berjalan
- optimistic UI membuat layar terasa sudah berubah lebih dulu
Keduanya sering dipakai bersama.
Contoh penggunaan useTransition
'use client'
import { useTransition } from 'react'
export function SaveButton({ action }: { action: (formData: FormData) => Promise<void> }) {
const [isPending, startTransition] = useTransition()
return (
<form action={(formData) => startTransition(() => action(formData))}>
<input name="title" />
<button type="submit" disabled={isPending}>
{isPending ? 'Menyimpan...' : 'Simpan'}
</button>
</form>
)
}Kalau form submit mengubah data yang dirender ulang dari server, pending state biasanya cukup untuk memberi feedback tanpa harus menunggu full page reload mental di kepala user.
Jebakan umum saat menunggu response server
1. UI terlihat cepat, tapi data final belum sinkron
Optimistic update bisa membuat UI tampak benar padahal data server belum selesai. Kalau revalidation tidak dipanggil atau scope-nya salah, data final bisa tertinggal.
2. Optimistic state tidak punya rollback
Kalau request gagal, state sementara harus dibatalkan. Kalau tidak, user melihat data palsu yang seolah-olah sudah tersimpan.
3. Pending state terlalu luas
Kadang satu pending flag dipakai untuk semua area UI. Akibatnya, satu submit kecil malah mematikan seluruh form atau halaman.
4. Double submit
Tanpa disable button atau kontrol in-flight request, user bisa mengirim request dua kali. Ini sering berujung duplikasi data.
5. Error server tidak dipetakan ke form
Error dari server yang hanya dilempar sebagai exception sering hilang dari UX. Lebih baik error dikembalikan dalam bentuk yang bisa dibaca komponen form.
6. Form client terlalu gemuk
Semua logic ditaruh di client, padahal sebagian besar harusnya server-side. Hasilnya bundle membengkak dan validation logic jadi berulang.
7. Terlalu cepat menganggap response server sebagai sumber kebenaran satu-satunya
Dalam alur optimistic, UI lokal memang boleh mendahului server. Yang penting adalah final reconciliation: data server tetap harus jadi referensi akhir.
Pola keputusan praktis
Pakai aturan singkat ini saat merancang form:
- butuh interaksi lokal cepat → kelola di client
- butuh mutation data → pakai Server Action
- ingin UI terasa instan → tambahkan optimistic UI
- ingin user paham request masih berjalan → tambahkan pending state
- ingin data konsisten setelah mutation → revalidate path atau tag yang relevan
Kalau form Anda hanya memvalidasi lalu mengirim data, Server Action sering cukup. Kalau ada interaksi kompleks sebelum submit, gabungkan client state dengan Server Action.
Alur produksi yang sehat
Alur yang umum dipakai di proyek nyata biasanya begini:
- user mengisi form di client
- client menjalankan validasi ringan dan menyiapkan pending state
- form submit ke Server Action
- UI menampilkan optimistic update bila cocok
- server memvalidasi ulang dan menyimpan data
- server mengembalikan hasil atau error yang jelas
- cache atau segment yang terdampak direvalidate
- UI final disinkronkan dengan data server
Kalau urutan ini dijaga, form terasa cepat tanpa mengorbankan konsistensi.
Catatan implementasi
- Jangan menyimpan secret atau logic sensitif di client hanya demi kenyamanan submit.
- Jangan pakai optimistic UI untuk operasi yang sulit dibatalkan kalau gagal.
- Jangan lupa bedakan error validasi dengan error server/transport.
- Jangan matikan semua kontrol form hanya karena satu field sedang pending.
- Jangan lupa revalidate kalau mutation memengaruhi data yang dibaca ulang di route lain.
Referensi terkait
- Next.js Routing, Server/Client Components, dan Data Fetching
- Next.js Caching, Revalidation, Server Actions, dan Mutations Flow
- React.js Forms: Controlled vs Uncontrolled, Lifting State, Validation, dan Submit Flow
- Fetch API, Async Data Flow, JSON Parsing, dan Error Handling Dasar