Lompat ke konten
Kembali ke modul

Next.js: Fullstack dalam Satu Proyek · 5/9

Mengubah data dengan Server Action

Function yang jalan di server, dipanggil dari sebuah form. Plus validasi yang tidak boleh kamu lewatkan.

Baca 32 menit

Setelah pelajaran ini kamu bisa

  • Menulis Server Action dan memanggilnya dari sebuah form
  • Menjelaskan kenapa setiap action harus melakukan authorization sendiri
  • Memvalidasi input di batasnya
  • Melakukan revalidate dan redirect setelah mutasi

Server Action — dokumentasinya juga menyebutnya Server Function — adalah function async yang ditandai "use server", jalan di server, dan bisa dipanggil dari client. Ia menghilangkan kebutuhan menulis route API untuk form milikmu sendiri.

tsx
// features/venue/actions.ts
"use server";                        // marks EVERY export in this file

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { getSessionUser } from "@/features/auth/session";

export async function createVenue(formData: FormData) {
  // 1. AUTHORISE. Always. See the warning below.
  const user = await getSessionUser();
  if (!user) return { error: "notSignedIn" };
  if (user.role !== "lead") return { error: "notAllowed" };

  // 2. VALIDATE. Everything in formData came from the browser.
  const name = formData.get("name");
  if (typeof name !== "string" || name.trim().length < 2) {
    return { error: "nameTooShort" };
  }

  // 3. Do the work.
  const venue = await insertVenue({ name: name.trim(), ownerId: user.id });

  // 4. Invalidate what is now stale, then navigate.
  revalidatePath("/venues");
  redirect("/venues/" + venue.id);
}
Coba sendiri

Dua action, fitur yang sama. Satu memercayai pemanggilnya; satunya tidak. Lihat apa yang dicapai request palsu terhadap masing-masingnya.

Hasil

Tekan Jalankan untuk melihat hasilnya.

Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.

Perhatikan versi aman mengambil identitas dari session, bukan dari payload. Action yang menerima argument userId lalu memercayainya adalah IDOR — siapa pun bisa mengirim id orang lain. Ini persis alasan getMyProgressAction di proyek ini tidak menerima parameter sama sekali.

Memanggil sebuah action

tsx
// 1. Directly as a form action. Works without JavaScript.
<form action={createVenue}>
  <input name="name" />
  <button type="submit">Create</button>
</form>

// 2. With useActionState, for errors and a pending flag.
"use client";
const [result, formAction, pending] = useActionState(createVenue, undefined);
<form action={formAction}>...</form>

// 3. Called from an event handler, for non-form mutations.
"use client";
function DeleteButton({ id }) {
  const [pending, startTransition] = useTransition();
  return (
    <button
      disabled={pending}
      onClick={() => startTransition(() => deleteVenue(id))}
    >
      Delete
    </button>
  );
}
Opsi 1 adalah progressive enhancement: form-nya ter-submit bahkan sebelum JavaScript termuat, karena ia benar-benar sebuah HTML form post.

Setelah mutasi

PemanggilanFungsinya
revalidatePath("/venues")Menandai data cache path itu jadi usang
revalidateTag("venues")Menginvalidasi berdasarkan tag, stale-while-revalidate
updateTag("venues")Kedaluwarsa segera, jadi pengguna melihat hasil tulisannya sendiri. Hanya di action.
redirect("/venues/1")Berpindah halaman. Ia melempar, jadi tidak ada yang jalan setelahnya.
return { error }Mengirim sebuah nilai kembali untuk ditampilkan form-nya

Tugas praktik

Di proyek ini, buka features/progress/actions.ts. Perhatikan setLessonCompleteAction membaca user dari session dan memvalidasi id pelajaran dengan isKnownLessonId sebelum menulis. Sekarang coba rusak: panggil dengan id pelajaran yang tidak ada dan pastikan tidak ada yang tertulis. Lalu baca komentar di atas getMyProgressAction yang menjelaskan kenapa ia tidak menerima id user.

Hasil yang diharapkan

Id yang tidak dikenal diabaikan, dan progres yang ada dikembalikan tanpa perubahan.