Form là một trong những component phổ biến của một website, đặc biệt trong việc thao tác update dữ liệu, form để đăng nhập, đăng ký, thêm bản ghi... Nhưng form cũng là phần phức tạp khi xử lý, ta ko thể tin tưởng client input nên phải validate form, validate form trên server là bắt buộc phải làm, nhưng muốn tăng UX thì cần phải validate form ở cả client-side...

Basic setup

Một số thư viện mình sẽ sử dụng trong bài viết này:

  • shadcn/ui - thư viện UI
  • zod - validation
  • react-hook-form - Quản lý state, bắt sự kiện và integrate với Zod

Về cơ bản thì bạn có thể áp dụng cách làm của mình trong hầu hết các dự án sử dụng React chỉ khác ở chỗ Server Action - cho framework Next.js (hoặc React experimental)

Go ahead cài đặt các thư viện cần thiết nào:

Parsing source code...

Với shadcn/ui thì mình cần component nào thì download component ấy về thôi (làm theo hướng dẫn trên trang chủ của shadcn tại đây để cài đặt.)

Schema

Bài viết này mình sẽ làm form cho chức năng Thêm hóa đơn, nên sẽ có các trường để nhập dữ liệu:

  • Mô tả (description)
  • Ngày thêm (date)
  • Loại (category)
  • Số tiền (amount)
  • Phương thức thanh toán (paymentMethod)

Giờ thì tạo schema cho các trường trên để phục vụ cho việc validation sau này, sử dụng thư viện zod, đây là schema mà mình tạo:

schema.ts
Parsing source code...

Phần schema trên sẽ được sử dụng để validate ở cả frontend và backend, nhờ có zod nên việc validation đỡ cực hơn nhiều, với một số trường hợp mà kiểu dữ liệu ở client khác với server (ví dụ kiểu dữ liệu ảnh) thì ta cần tách phần khác nhau ra làm 2 schema (bài này chủ yếu nói về form nên case đặc biệt này mình sẽ nói ở bài khác về validation riêng.)

Giao diện

Chuẩn bị

Bên trên mình đã nói rằng ta sẽ sử dụng thư viện shadcn/ui để dựng UI, giờ là lúc download các components cần thiết để tạo thành một cái form theo ý mình:

  • form
  • button (submit button)
  • date-picker (cho field date) - thằng này được tạo bởi 2 thằng: popover và calendar
  • select (cho field paymentMethod)
  • input (cho các field text, number)

Go ahead then download them:

Parsing source code...

Coding giao diện

Khởi tạo form với react-hook-form:

Parsing source code...

Tạo giao diện bắt đầu với form component của shadcn-ui download bên trên, có một số case đặc biệt ở đây: các component đặc biệt trong form như select hay datepicker không nhận giá trị khi submit, có nghĩa là bạn vẫn chọn được giá trị như bình thường như khi submit thì dữ liệu ấy ko được gửi lên server, có 1 trick or hack để fix cái issue đó là thêm một thẻ hidden input ngay dưới các component đặc biệt này:

Parsing source code...

Kết hợp với validation của zod mình viết bên trên thì cuối cùng ta sẽ có một cái form hoàn chỉnh như sau:

AddInvoiceForm.tsx
Parsing source code...

Server action

Phần này là riêng cho Next.js (hoặc React bản experimental có hỗ trợ Server actions) bạn có thể hiểu tương tự như phần xử lý ở phía backend end khi người dùng submit form. Thường thì bạn sẽ viết một cái API ở backend để handle cho 1 cái form tương ứng, với Next.js server action nó sẽ tự generate cho bạn một đầu API tương ứng - nhiệm vụ của bạn chỉ cần viết logic update dữ liêu ko cần phải khai báo route mới ở controller. Có một số điều cần lưu ý về thằng Server action này:

  • Các đầu API generated đều là public API (nên user có thể gọi và truyền tham số tùy ý bằng Postman hoặc các API client tương tự) -> cần lưu ý authorize trước khi thực hiện function
  • Action chỉ có sử dụng POST method API
  • Action có thể được gọi ở Client component, vậy nên tham số truyền vào phải có thể serializable (có thể chuyển đổi định dạng) - nôm na là các tham số này sẽ được đọc bởi Server action - function (trên server) nên là bạn cần phải truyền vào kiểu dữ liệu mà server có thể đọc được (giống như bạn ko thể truyền function vào JSON file rồi gửi cho client để chạy được), xem các kiểu dữ liệu hỗ trợ ở đây

Alright, dài dòng vậy thôi mà cuối cùng thì Server action cũng chỉ là function nên code đơn giản thế này thôi (đừng quên thêm use-server ở đầu file)

lib/actions.js
Parsing source code...

Kết hợp form với Server action

Giờ thì kết hợp Server action và form UI mà được viết ở trên với nhau, thêm function onSubmit vào (đây là cách tốt nhất hiện tại theo mình nghĩ), còn với cách được đề xuất trên trang của Next.js thì bạn ko cần phải khai báo function onSubmit mà truyền thẳng thằng action vào form element luôn, nhưng mà nó có nhược điểm mình sẽ trình bày sau.

AddInvoiceForm.tsx
Parsing source code...

Oke có thể bạn không để ý rằng form bên trên thiếu submit button, mình muốn thêm state cho cái button này nên viết riêng ra một component vậy:

Parsing source code...

Rồi update form thêm cái button vào là xong, isSubmitting mình sẽ đọc từ formState của react-hook-form

Parsing source code...

That's it, giờ ta đã có một cái form hoàn chỉnh được validate 2 phía cả client và server, bạn có thể thêm logic như navigate đến trang mới khi submit thành công trong hàm onSubmit.

Bonus

Giờ mình sẽ so sánh cách ở trên với cách được Next.js đề xuất (được viết trong docs của Next.js). Với cách được đề xuất của Next.js ta sẽ dùng thêm 2 function mới của react-domuseFormStateuseFormStatus, ko cần phải viết function onSubmit nữa. Dưới đây là code cho phần thay đổi đó:

Parsing source code...

Và submit button của ta cũng cần phải update một chút, lúc này submit button ko cần phải truyền isSubmitting từ bên ngoài vào mà có thể dùng hàm useFormStatus để lấy trạng thái submit. Here is the code:

Parsing source code...

info

  • Chí có một issue với cách trên là nó ko trigger validate ở client khi bạn submit form, khi bạn nhấn submit button thì ngay lập tức nó sẽ gửi request lên server (ko validate gì ở client cả)

  • Có thể bạn có trick để trigger validate ở client trước khi gọi request lên server, thử implement xem khả năng cao phần useFormStatus trong submit button sẽ ko hoạt động (vì mình thử rồi!)