Kuasai validasi React Server Action. Panduan mendalam tentang pemrosesan formulir, praktik terbaik keamanan, dan teknik canggih menggunakan Zod, useFormState, dan useFormStatus.
Validasi React Server Action: Panduan Komprehensif untuk Pemrosesan Input Formulir dan Keamanan
Pengenalan React Server Actions telah menandai pergeseran paradigma yang signifikan dalam pengembangan full-stack dengan kerangka kerja seperti Next.js. Dengan mengizinkan komponen klien untuk memanggil fungsi sisi server secara langsung, kita sekarang dapat membangun aplikasi yang lebih kohesif, efisien, dan interaktif dengan lebih sedikit boilerplate. Namun, abstraksi baru yang kuat ini membawa tanggung jawab kritis ke garis depan: validasi input dan keamanan yang tangguh.
Ketika batas antara klien dan server menjadi begitu mulus, mudah untuk mengabaikan prinsip-prinsip dasar keamanan web. Setiap input yang datang dari pengguna tidak dapat dipercaya dan harus diverifikasi secara ketat di server. Panduan ini memberikan eksplorasi komprehensif tentang pemrosesan dan validasi input formulir dalam React Server Actions, mencakup segalanya mulai dari prinsip dasar hingga pola canggih yang siap produksi yang memastikan aplikasi Anda ramah pengguna dan aman.
Apa Sebenarnya React Server Actions itu?
Sebelum mendalami validasi, mari kita rekap secara singkat apa itu Server Actions. Pada intinya, mereka adalah fungsi yang Anda definisikan di server tetapi dapat dieksekusi dari klien. Ketika pengguna mengirimkan formulir atau mengklik tombol, Server Action dapat dipanggil secara langsung, melewati kebutuhan untuk membuat endpoint API secara manual, menangani permintaan `fetch`, dan mengelola status pemuatan/error.
Mereka dibangun di atas fondasi formulir HTML dan API `FormData` dari Platform Web, membuatnya ditingkatkan secara progresif secara default. Ini berarti formulir Anda akan tetap berfungsi bahkan jika JavaScript gagal dimuat, memberikan pengalaman pengguna yang tangguh.
Contoh Server Action dasar:
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// ... logika untuk menyimpan pengguna ke database
console.log('Creating user:', { name, email });
}
// app/page.js
import { createUser } from './actions';
export default function UserForm() {
return (
);
}
Kesederhanaan ini sangat kuat, tetapi juga menyembunyikan kompleksitas dari apa yang sedang terjadi. Fungsi `createUser` berjalan secara eksklusif di server, namun dipanggil dari komponen klien. Jalur langsung ke logika server Anda inilah yang menjadi alasan mengapa validasi bukan hanya sebuah fitur—itu adalah sebuah keharusan.
Pentingnya Validasi yang Tak Tergoyahkan
Dalam dunia Server Actions, setiap fungsi adalah gerbang terbuka ke server Anda. Validasi yang tepat bertindak sebagai penjaga di gerbang itu. Inilah mengapa hal itu tidak bisa ditawar:
- Integritas Data: Database dan status aplikasi Anda bergantung pada data yang bersih dan dapat diprediksi. Validasi memastikan bahwa Anda tidak menyimpan alamat email yang salah format, string kosong di tempat yang seharusnya nama, atau teks di bidang yang dimaksudkan untuk angka.
- Pengalaman Pengguna (UX) yang Ditingkatkan: Pengguna membuat kesalahan. Pesan error yang jelas, segera, dan spesifik konteks membimbing mereka untuk memperbaiki input mereka, mengurangi frustrasi dan meningkatkan tingkat penyelesaian formulir.
- Keamanan yang Kokoh: Ini adalah aspek yang paling kritis. Tanpa validasi sisi server, aplikasi Anda rentan terhadap berbagai serangan, termasuk:
- SQL Injection: Aktor jahat dapat mengirimkan perintah SQL di bidang formulir untuk memanipulasi database Anda.
- Cross-Site Scripting (XSS): Jika Anda menyimpan dan merender input pengguna yang tidak disanitasi, penyerang dapat menyuntikkan skrip berbahaya yang dieksekusi di browser pengguna lain.
- Denial of Service (DoS): Mengirimkan data yang tiba-tiba besar atau mahal secara komputasi dapat membebani sumber daya server Anda.
Validasi Sisi Klien vs. Sisi Server: Kemitraan yang Diperlukan
Penting untuk dipahami bahwa validasi harus terjadi di dua tempat:
- Validasi Sisi Klien: Ini untuk UX. Ini memberikan umpan balik instan tanpa perjalanan bolak-balik jaringan. Anda dapat menggunakan atribut HTML5 sederhana seperti `required`, `minLength`, `pattern`, atau JavaScript untuk memeriksa format saat pengguna mengetik. Namun, ini dapat dengan mudah dilewati dengan menonaktifkan JavaScript atau menggunakan alat pengembang.
- Validasi Sisi Server: Ini untuk keamanan dan integritas data. Ini adalah sumber kebenaran utama aplikasi Anda. Tidak peduli apa yang terjadi di klien, server harus memvalidasi ulang semua yang diterimanya. Server Actions adalah tempat yang sempurna untuk mengimplementasikan logika ini.
Aturan praktis: Gunakan validasi sisi klien untuk pengalaman pengguna yang lebih baik, tetapi selalu hanya percayai validasi sisi server untuk keamanan.
Mengimplementasikan Validasi di Server Actions: Dari Dasar hingga Lanjutan
Mari kita bangun strategi validasi kita, mulai dari pendekatan sederhana dan beralih ke solusi yang lebih tangguh dan dapat diskalakan menggunakan alat modern.
Pendekatan 1: Validasi Manual dan Mengembalikan State
Cara paling sederhana untuk menangani validasi adalah dengan menambahkan pernyataan `if` di dalam Server Action Anda dan mengembalikan objek yang menunjukkan keberhasilan atau kegagalan.
// app/actions.js
'use server';
import { redirect } from 'next/navigation';
export async function createInvoice(formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
if (!customerName || customerName.trim() === '') {
return { success: false, message: 'Customer name is required.' };
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
return { success: false, message: 'Please enter a valid amount greater than zero.' };
}
// ... logika untuk membuat faktur di database
console.log('Invoice created for', customerName, 'with amount', amount);
redirect('/dashboard/invoices');
}
Pendekatan ini berhasil, tetapi memiliki kelemahan UX yang besar: memerlukan pemuatan ulang halaman penuh untuk menampilkan pesan error. Kita tidak dapat dengan mudah menampilkan pesan di halaman formulir itu sendiri. Di sinilah hook React untuk Server Actions berperan.
Pendekatan 2: Menggunakan `useFormState` untuk Penanganan Error yang Mulus
Hook `useFormState` dirancang khusus untuk tujuan ini. Ini memungkinkan Server Action untuk mengembalikan state yang dapat digunakan untuk memperbarui UI tanpa peristiwa navigasi penuh. Ini adalah landasan penanganan formulir modern dengan Server Actions.
Mari kita refactor formulir pembuatan faktur kita.
Langkah 1: Perbarui Server Action
Action sekarang perlu menerima dua argumen: `prevState` dan `formData`. Ini harus mengembalikan objek state baru yang akan digunakan `useFormState` untuk memperbarui komponen.
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Tentukan bentuk state awal
const initialState = {
message: null,
errors: {},
};
export async function createInvoice(prevState, formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
const status = formData.get('status');
const errors = {};
if (!customerName || customerName.trim().length < 2) {
errors.customerName = 'Customer name must be at least 2 characters.';
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
errors.amount = 'Please enter a valid amount.';
}
if (status !== 'pending' && status !== 'paid') {
errors.status = 'Please select a valid status.';
}
if (Object.keys(errors).length > 0) {
return {
message: 'Failed to create invoice. Please check the fields.',
errors,
};
}
try {
// ... logika untuk menyimpan ke database
console.log('Invoice created successfully!');
} catch (e) {
return {
message: 'Database Error: Failed to create invoice.',
errors: {},
};
}
// Validasi ulang cache untuk halaman faktur dan alihkan
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
Langkah 2: Perbarui Komponen Formulir dengan `useFormState`
Di komponen klien kita, kita akan menggunakan hook untuk mengelola state formulir dan menampilkan error.
// app/ui/invoices/create-form.js
'use client';
import { useFormState } from 'react-dom';
import { createInvoice } from '@/app/actions';
const initialState = {
message: null,
errors: {},
};
export function CreateInvoiceForm() {
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
);
}
Sekarang, ketika pengguna mengirimkan formulir yang tidak valid, Server Action berjalan, mengembalikan objek error, dan `useFormState` memperbarui variabel `state`. Komponen dirender ulang, menampilkan pesan error spesifik tepat di sebelah bidang yang sesuai—semua tanpa pemuatan ulang halaman. Ini adalah peningkatan UX yang luar biasa!
Pendekatan 3: Meningkatkan UX dengan `useFormStatus`
Apa yang terjadi saat Server Action sedang berjalan? Pengguna mungkin mengklik tombol kirim beberapa kali. Kita dapat memberikan umpan balik menggunakan hook `useFormStatus`, yang memberi kita informasi tentang status pengiriman formulir terakhir.
Penting: `useFormStatus` harus digunakan dalam komponen yang merupakan anak dari elemen `