Nguyên tắc: một nguồn dữ liệu duy nhất cho cả wizard, mỗi bước chỉ là một khung nhìn của nó.
State. Giữ toàn bộ giá trị ở form cha (FormProvider của react-hook-form, hoặc một reducer), mỗi bước đọc/ghi qua context. Nếu mỗi bước tự giữ state riêng thì quay lại bước trước sẽ mất dữ liệu và bước cuối không có gì để submit.
Validate theo bước. Tách schema thành từng mảnh theo bước, khi bấm "Tiếp" chỉ validate các field của bước đó:
const methods = useForm({ mode: 'onBlur', defaultValues: draft })
const next = async () => {
const ok = await methods.trigger(FIELDS_BY_STEP[step])
if (ok) setStep((s) => s + 1)
}Trước khi gọi API thì validate lại toàn bộ schema — người dùng có thể quay lại sửa bước 1 rồi nhảy thẳng tới bước cuối.
Lưu nháp. Ghi vào localStorage với debounce (~500ms) thay vì mỗi phím gõ, kèm version của schema và mốc thời gian để bỏ nháp quá cũ hoặc không còn khớp cấu trúc:
useEffect(() => {
const sub = methods.watch((values) => saveDraftDebounced({ v: 2, at: Date.now(), values }))
return () => sub.unsubscribe()
}, [methods])Hai điểm hay bị hỏi thêm: không lưu dữ liệu nhạy cảm (mật khẩu, số thẻ) vào nháp; và đưa step lên URL (?step=2) để nút Back của trình duyệt lùi một bước thay vì thoát hẳn khỏi form.
Xoá nháp ngay khi submit thành công.