nextjsserver-actionsuxMahir4 mnt baca

Optimistic Update

UI langsung update sebelum server merespons dengan useOptimistic.

Idenya: jangan suruh user menunggu

Rata-rata request server butuh 200-500ms. Untuk aksi kecil seperti like, centang todo, atau tambah ke favorit, jeda itu terasa "lemot" walau sebenarnya normal. Optimistic update membalik urutannya: UI langsung berubah seolah server sudah sukses, request-nya menyusul di belakang layar. Analoginya seperti kasir yang langsung memberimu struk sebelum uangmu selesai dihitung, karena ia percaya transaksinya akan beres.

React menyediakan useOptimistic untuk ini. Ia menerima state asli dan sebuah fungsi reducer, lalu mengembalikan salinan "optimistik" plus fungsi untuk memicunya.

Contoh nyata: toggle todo

Berbeda dengan contoh like sederhana, kasus nyata biasanya bekerja pada array. Reducer-nya menerima state sekarang dan payload aksi:

tsx
// app/todos/TodoList.tsx
"use client";
import { useOptimistic } from "react";
import { toggleTodo } from "./actions";

type Todo = { id: string; judul: string; selesai: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimistik, setOptimistik] = useOptimistic(
    todos,
    (state: Todo[], id: string) =>
      state.map((t) => (t.id === id ? { ...t, selesai: !t.selesai } : t))
  );

  return (
    <ul>
      {optimistik.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.selesai}
              onChange={async () => {
                setOptimistik(todo.id);      // UI langsung berubah
                await toggleTodo(todo.id);   // server menyusul
              }}
            />
            {todo.judul}
          </label>
        </li>
      ))}
    </ul>
  );
}

Dan action-nya tetap sederhana, plus revalidatePath agar data server sinkron setelahnya:

tsx
// app/todos/actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function toggleTodo(id: string) {
  await new Promise((r) => setTimeout(r, 800)); // simulasi latensi
  await db.todo.update(id);
  revalidatePath("/todos");
}

Perhatikan urutannya di onChange: setOptimistik dipanggil DULU tanpa await (ia sinkron dan instan), baru request server dikirim. Kalau dibalik, efek instannya hilang.

Kalau server gagal?

Inilah bagian yang paling sering dilupakan. Kalau toggleTodo melempar error, UI optimistikmu sedang berbohong kepada user. Pola amannya: bungkus dengan try/catch, dan saat gagal, paksa sinkronisasi ulang dari server:

tsx
// app/todos/TodoList.tsx (di dalam onChange)
import { useRouter } from "next/navigation";

const router = useRouter();
// ...
onChange={async () => {
  setOptimistik(todo.id);
  try {
    await toggleTodo(todo.id);
  } catch {
    router.refresh(); // tarik data asli dari server, UI kembali jujur
    alert("Gagal menyimpan, coba lagi");
  }
}}

router.refresh() me-render ulang Server Component dan menarik data segar, sehingga checkbox kembali ke kondisi sebenarnya di database.

Kasus lain yang perlu dipikirkan: user mengklik checkbox lima kali dalam satu detik. Setiap klik memicu setOptimistik dan satu request server. Karena reducer selalu dihitung dari state terakhir, UI tetap konsisten tanpa lompatan aneh. Request yang menumpuk diselesaikan berurutan oleh server, dan revalidatePath di akhir memastikan data final sinkron. Inilah alasan reducer harus murni dan idempoten: ia bisa dipanggil berkali-kali dengan hasil yang dapat diprediksi.

Catatan teknis: Optimistic update hanya pantas untuk aksi yang kemungkinan besar berhasil dan mudah dibatalkan: like, centang todo, arsip email. Untuk pembayaran, transfer saldo, atau pengurangan stok, JANGAN optimistik. Tunggu konfirmasi server dulu, karena "UI berbohong soal uang" adalah bug yang mahal.

Tantangan

Todo optimistik

Buat daftar todo di mana mencentang langsung mengubah UI via useOptimistic, sementara Server Action menyimpan ke server (beri delay 1 detik untuk melihat efeknya).