Validation. Tôi tách hai thời điểm: validate on-blur/on-submit cho lỗi nghiêm trọng (đừng bắn đỏ ngay khi người dùng vừa bắt đầu gõ), và validate on-change cho phản hồi tức thì ở field đơn giản.
- Với form lớn tôi dùng
react-hook-form(uncontrolled, ít re-render) + một schema (zod) làm nguồn sự thật duy nhất cho luật, dùng được cho cả client và server để không phải viết luật hai lần. - Luôn xác thực lại ở server — validation client chỉ là UX.
tsx
const schema = z.object({ title: z.string().min(1), body: z.string() })
const { register, watch } = useForm({ resolver: zodResolver(schema) })
useEffect(() => {
const sub = watch(debounce((values) => {
localStorage.setItem('draft', JSON.stringify(values)) // + PATCH to server
}, 1500))
return () => sub.unsubscribe()
}, [watch])Autosave / draft. Mục tiêu là không mất dữ liệu người dùng khi họ đóng tab hoặc mất mạng.
- Debounce việc lưu: lưu draft sau khi người dùng ngừng gõ ~1–2s, không lưu theo từng phím. Chỉ gửi khi form ở trạng thái hợp lệ tối thiểu (hoặc lưu draft thô nhưng đánh dấu chưa hoàn tất).
- Nơi lưu:
localStoragecho draft nhanh phía client (offline vẫn giữ), hoặc PATCH lên server nếu cần đồng bộ đa thiết bị. Nhiều sản phẩm dùng cả hai: local trước cho tức thì, sync server nền. - Chỉ báo trạng thái: hiện "Đang lưu…/Đã lưu lúc HH:mm" để người dùng tin dữ liệu an toàn.
- Khôi phục draft: khi mở lại form, phát hiện draft và hỏi "Khôi phục bản nháp?" thay vì tự áp đè.
- Race: nếu autosave và submit có thể chạy chồng nhau, huỷ autosave đang chờ khi submit, và dùng cùng cơ chế so khớp/abort như search để bản lưu cũ không ghi đè bản mới.
Đừng chặn người dùng rời trang bằng dialog trừ khi thật sự có thay đổi chưa lưu (beforeunload), và chỉ khi autosave chưa kịp chạy.