Mục lục

Multi-step Form: Xử lý Form nhiều bước (Wizard)

Hướng dẫn xây dựng Form Wizard (Step 1 -> Step 2 -> Submit). Quản lý State giữa các bước, Validate từng phần với trigger(), và Animation chuyển cảnh.

Form đăng ký quá dài? Chia nhỏ nó ra thành nhiều bước (Steps) để user đỡ ngán.

1. Kiến trúc State (State Lifting)

Sai lầm phổ biến: Mỗi Step là một Form riêng biệt. -> Dữ liệu bị phân mảnh. Khó submit ở bước cuối.

Giải pháp đúng: Một useForm đặt ở Component Cha. Các Step chỉ là Component Con nhận register/control từ cha.

tsx:
function WizardForm() {
  const { register, trigger, handleSubmit } = useForm<WizardData>();
  const [step, setStep] = useState(1);

  const nextStep = async () => {
    // 🔥 Magic: Validate CHỈ những trường của bước hiện tại
    let isValid = false;
    
    if (step === 1) isValid = await trigger(["firstName", "lastName"]);
    if (step === 2) isValid = await trigger(["email", "phone"]);

    if (isValid) setStep(s => s + 1);
  };

  return (
    <form onSubmit={handleSubmit(finalSubmit)}>
      {/* Chỉ hiển thị Step hiện tại */}
      {step === 1 && <Step1 register={register} />}
      {step === 2 && <Step2 register={register} />}
      
      {step < 3 && <button onClick={nextStep}>Next</button>}
      {step === 3 && <button type="submit">Finish</button>}
    </form>
  )
}

2. Validate từng phần (trigger)

Hàm trigger("field_name") của RHF cực kỳ hữu dụng ở đây. Nó chạy validation Zod cho đúng field bạn chỉ định.

  • Nếu Step 1 user nhập sai -> trigger trả về false -> Không cho next.
  • Ngăn chặn việc sang Step 3 rồi mới báo lỗi ở Step 1.

3. Bảo lưu dữ liệu (Persist Data)

Khi user bấm Back từ Step 2 về Step 1, dữ liệu Step 1 phải còn nguyên. Vì ta dùng kiến trúc State Lifting (useForm ở cha), dữ liệu luôn nằm trong bộ nhớ của Cha. Việc unmount Step1 rồi mount lại không làm mất dữ liệu trong RHF Store.

4. UX & Animation

Để Wizard xịn sò hơn, hãy thêm Animation trượt ngang (Slide) với Framer Motion.

tsx:
<AnimatePresence mode="wait">
  <motion.div
    key={step}
    initial={{ x: 50, opacity: 0 }}
    animate={{ x: 0, opacity: 1 }}
    exit={{ x: -50, opacity: 0 }}
  >
    {step === 1 && <Step1 ... />}
  </motion.div>
</AnimatePresence>

Kết luận

Multi-step Form không khó về kỹ thuật (vẫn là 1 form to). Nó chỉ khó về Validation Logic (đảm bảo step trước đúng thì mới cho qua step sau). Hàm trigger của RHF giải quyết trọn vẹn vấn đề này.

Quảng cáo
mdhorizontal