Skip to content

📅 NGÀY 45: ⚡ Project 6 - Registration Flow

🎯 Mục tiêu học tập (5 phút)

  • [ ] Tổng hợp React Hook Form + Zod + Context vào real-world project
  • [ ] Implement multi-step wizard form hoàn chỉnh
  • [ ] Quản lý complex form state across multiple steps
  • [ ] Validate mỗi step independently với Zod schemas
  • [ ] Handle file uploads trong forms
  • [ ] Build production-ready registration flow

🤔 Kiểm tra đầu vào (5 phút)

  1. React Hook Form useForm hook có những options nào quan trọng?
  2. Zod schema composition hoạt động như thế nào?
  3. Context API pattern nào tốt nhất cho form state management?

📖 PHẦN 1: PROJECT OVERVIEW (30 phút)

1.1 Vấn Đề Thực Tế

Real-world Registration Flow Requirements:

jsx
// ❌ BAD: Single long form
function BadRegistrationForm() {
  return (
    <form>
      {/* 20+ fields all at once */}
      <input name='firstName' />
      <input name='lastName' />
      <input name='email' />
      <input name='password' />
      <input name='confirmPassword' />
      <input name='phone' />
      <input name='address' />
      <input name='city' />
      <input name='country' />
      {/* ... 11 more fields */}
      <button>Submit All</button>
    </form>
  );
}

// Problems:
// 1. User overwhelmed - "Too many fields!"
// 2. No progress indication
// 3. Validation all at once = confusing errors
// 4. Can't save partial progress
// 5. Mobile UX terrible
// 6. High abandonment rate

Real-world Examples:

E-commerce Checkout:
Step 1: Shipping Address
Step 2: Payment Info
Step 3: Review Order
Step 4: Confirmation

Job Application:
Step 1: Personal Info
Step 2: Education
Step 3: Work Experience
Step 4: Documents Upload
Step 5: Review & Submit

Account Setup:
Step 1: Basic Info
Step 2: Profile Details
Step 3: Preferences
Step 4: Verification

1.2 Giải Pháp: Multi-step Wizard

✅ GOOD: Progressive disclosure

jsx
// Step-by-step approach
<RegistrationWizard>
  <Step1_PersonalInfo /> {/* 4-5 fields */}
  <Step2_AccountDetails /> {/* 3-4 fields */}
  <Step3_Preferences /> {/* 3-4 fields */}
  <Step4_Review /> {/* Summary */}
</RegistrationWizard>

// Benefits:
// ✅ Less overwhelming
// ✅ Progress indication
// ✅ Validate per step
// ✅ Save partial progress
// ✅ Better mobile UX
// ✅ Higher completion rate

1.3 Project Architecture

Component Structure:

RegistrationFlow/
├── RegistrationWizard.jsx       // Main orchestrator
├── FormContext.jsx              // Shared state
├── steps/
│   ├── Step1_PersonalInfo.jsx
│   ├── Step2_Contact.jsx
│   ├── Step3_Account.jsx
│   ├── Step4_Preferences.jsx
│   └── Step5_Review.jsx
├── components/
│   ├── ProgressBar.jsx
│   ├── StepIndicator.jsx
│   └── NavigationButtons.jsx
└── validation/
    └── schemas.js               // Zod schemas

1.4 Technical Requirements

Must Have:

  • React Hook Form for each step
  • Zod validation per step
  • Context for state management
  • Progress indication
  • Navigation (Next/Back/Submit)
  • Data persistence across steps
  • Review & edit capability
  • Form submission

Nice to Have:

  • File upload (profile picture)
  • Auto-save to localStorage
  • Field-level validation feedback
  • Smooth transitions
  • Mobile responsive

💻 PHẦN 2: PROJECT IMPLEMENTATION (120 phút)

Demo 1: Project Setup & Architecture ⭐

jsx
/**
 * Registration Flow - Complete Project Setup
 *
 * Features:
 * - Multi-step wizard (5 steps)
 * - React Hook Form + Zod
 * - Context for state management
 * - Progress indication
 * - Review & submit
 */

import { useState, createContext, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// ============= VALIDATION SCHEMAS =============

const personalInfoSchema = z.object({
  firstName: z
    .string()
    .min(2, 'First name must be at least 2 characters')
    .max(50, 'First name too long'),
  lastName: z
    .string()
    .min(2, 'Last name must be at least 2 characters')
    .max(50, 'Last name too long'),
  birthDate: z.string().refine((date) => {
    const age = new Date().getFullYear() - new Date(date).getFullYear();
    return age >= 18;
  }, 'Must be at least 18 years old'),
  gender: z.enum(['male', 'female', 'other', 'prefer-not-to-say']),
});

const contactSchema = z.object({
  email: z.string().email('Invalid email address').min(1, 'Email is required'),
  phone: z
    .string()
    .regex(/^\+?[1-9]\d{9,14}$/, 'Invalid phone number')
    .min(1, 'Phone is required'),
  address: z.string().min(5, 'Address too short').max(200, 'Address too long'),
  city: z.string().min(2, 'City required'),
  country: z.string().min(2, 'Country required'),
});

const accountSchema = z
  .object({
    username: z
      .string()
      .min(3, 'Username must be at least 3 characters')
      .max(20, 'Username too long')
      .regex(
        /^[a-zA-Z0-9_]+$/,
        'Only letters, numbers, and underscore allowed',
      ),
    password: z
      .string()
      .min(8, 'Password must be at least 8 characters')
      .regex(/[A-Z]/, 'Must contain uppercase letter')
      .regex(/[a-z]/, 'Must contain lowercase letter')
      .regex(/[0-9]/, 'Must contain number')
      .regex(/[^A-Za-z0-9]/, 'Must contain special character'),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ['confirmPassword'],
  });

const preferencesSchema = z.object({
  newsletter: z.boolean(),
  notifications: z.enum(['all', 'important', 'none']),
  language: z.enum(['en', 'vi', 'fr', 'es']),
  timezone: z.string().min(1, 'Timezone required'),
});

// ============= FORM CONTEXT =============

const FormContext = createContext();

function FormProvider({ children }) {
  const [formData, setFormData] = useState({
    personalInfo: {},
    contact: {},
    account: {},
    preferences: {},
  });

  const [currentStep, setCurrentStep] = useState(0);

  const updateFormData = (step, data) => {
    setFormData((prev) => ({
      ...prev,
      [step]: { ...prev[step], ...data },
    }));
  };

  const nextStep = () => setCurrentStep((prev) => Math.min(prev + 1, 4));
  const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 0));
  const goToStep = (step) => setCurrentStep(step);

  return (
    <FormContext.Provider
      value={{
        formData,
        updateFormData,
        currentStep,
        nextStep,
        prevStep,
        goToStep,
      }}
    >
      {children}
    </FormContext.Provider>
  );
}

function useFormContext() {
  const context = useContext(FormContext);
  if (!context) {
    throw new Error('useFormContext must be used within FormProvider');
  }
  return context;
}

// ============= PROGRESS INDICATOR =============

function ProgressBar({ currentStep, totalSteps }) {
  const progress = ((currentStep + 1) / totalSteps) * 100;

  return (
    <div style={{ marginBottom: 32 }}>
      {/* Progress bar */}
      <div
        style={{
          width: '100%',
          height: 8,
          backgroundColor: '#e5e7eb',
          borderRadius: 4,
          overflow: 'hidden',
        }}
      >
        <div
          style={{
            width: `${progress}%`,
            height: '100%',
            backgroundColor: '#3b82f6',
            transition: 'width 0.3s ease',
          }}
        />
      </div>

      {/* Step indicators */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          marginTop: 16,
        }}
      >
        {['Personal', 'Contact', 'Account', 'Preferences', 'Review'].map(
          (label, idx) => (
            <div
              key={idx}
              style={{
                flex: 1,
                textAlign: 'center',
                fontSize: 12,
                color: idx <= currentStep ? '#3b82f6' : '#9ca3af',
                fontWeight: idx === currentStep ? 'bold' : 'normal',
              }}
            >
              <div
                style={{
                  width: 32,
                  height: 32,
                  borderRadius: '50%',
                  backgroundColor: idx <= currentStep ? '#3b82f6' : '#e5e7eb',
                  color: idx <= currentStep ? 'white' : '#6b7280',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  margin: '0 auto 8px',
                  fontWeight: 'bold',
                }}
              >
                {idx + 1}
              </div>
              {label}
            </div>
          ),
        )}
      </div>
    </div>
  );
}

// ============= NAVIGATION BUTTONS =============

function NavigationButtons({
  onBack,
  onNext,
  isFirstStep,
  isLastStep,
  isValid,
}) {
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        marginTop: 32,
        paddingTop: 24,
        borderTop: '1px solid #e5e7eb',
      }}
    >
      <button
        type='button'
        onClick={onBack}
        disabled={isFirstStep}
        style={{
          padding: '12px 24px',
          backgroundColor: isFirstStep ? '#e5e7eb' : 'white',
          border: '1px solid #d1d5db',
          borderRadius: 8,
          cursor: isFirstStep ? 'not-allowed' : 'pointer',
          fontSize: 16,
          fontWeight: 500,
          color: isFirstStep ? '#9ca3af' : '#374151',
        }}
      >
        ← Back
      </button>

      <button
        type='submit'
        disabled={!isValid}
        style={{
          padding: '12px 24px',
          backgroundColor: isValid ? '#3b82f6' : '#93c5fd',
          color: 'white',
          border: 'none',
          borderRadius: 8,
          cursor: isValid ? 'pointer' : 'not-allowed',
          fontSize: 16,
          fontWeight: 600,
        }}
      >
        {isLastStep ? 'Submit' : 'Next →'}
      </button>
    </div>
  );
}

// Kết quả:
// ✅ Project structure ready
// ✅ Validation schemas defined
// ✅ Context for state management
// ✅ Progress indicator
// ✅ Navigation components

Demo 2: Individual Steps Implementation ⭐⭐

jsx
/**
 * Step Components - Each step is independent
 */

// ============= STEP 1: Personal Info =============

function Step1_PersonalInfo() {
  const { formData, updateFormData, nextStep } = useFormContext();

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm({
    resolver: zodResolver(personalInfoSchema),
    defaultValues: formData.personalInfo,
    mode: 'onChange',
  });

  const onSubmit = (data) => {
    updateFormData('personalInfo', data);
    nextStep();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2 style={{ marginBottom: 24 }}>Personal Information</h2>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: 16,
          marginBottom: 16,
        }}
      >
        <div>
          <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
            First Name *
          </label>
          <input
            {...register('firstName')}
            style={{
              width: '100%',
              padding: 12,
              border: `1px solid ${errors.firstName ? '#dc2626' : '#d1d5db'}`,
              borderRadius: 8,
              fontSize: 16,
            }}
          />
          {errors.firstName && (
            <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
              {errors.firstName.message}
            </p>
          )}
        </div>

        <div>
          <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
            Last Name *
          </label>
          <input
            {...register('lastName')}
            style={{
              width: '100%',
              padding: 12,
              border: `1px solid ${errors.lastName ? '#dc2626' : '#d1d5db'}`,
              borderRadius: 8,
              fontSize: 16,
            }}
          />
          {errors.lastName && (
            <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
              {errors.lastName.message}
            </p>
          )}
        </div>
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Birth Date *
        </label>
        <input
          type='date'
          {...register('birthDate')}
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.birthDate ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.birthDate && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.birthDate.message}
          </p>
        )}
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Gender *
        </label>
        <select
          {...register('gender')}
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.gender ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        >
          <option value=''>Select gender</option>
          <option value='male'>Male</option>
          <option value='female'>Female</option>
          <option value='other'>Other</option>
          <option value='prefer-not-to-say'>Prefer not to say</option>
        </select>
        {errors.gender && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.gender.message}
          </p>
        )}
      </div>

      <NavigationButtons
        onNext={handleSubmit(onSubmit)}
        isFirstStep={true}
        isValid={isValid}
      />
    </form>
  );
}

// ============= STEP 2: Contact =============

function Step2_Contact() {
  const { formData, updateFormData, nextStep, prevStep } = useFormContext();

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm({
    resolver: zodResolver(contactSchema),
    defaultValues: formData.contact,
    mode: 'onChange',
  });

  const onSubmit = (data) => {
    updateFormData('contact', data);
    nextStep();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2 style={{ marginBottom: 24 }}>Contact Information</h2>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Email Address *
        </label>
        <input
          type='email'
          {...register('email')}
          placeholder='you@example.com'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.email ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.email && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.email.message}
          </p>
        )}
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Phone Number *
        </label>
        <input
          type='tel'
          {...register('phone')}
          placeholder='+1234567890'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.phone ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.phone && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.phone.message}
          </p>
        )}
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Address *
        </label>
        <input
          {...register('address')}
          placeholder='Street address'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.address ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.address && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.address.message}
          </p>
        )}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
        <div>
          <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
            City *
          </label>
          <input
            {...register('city')}
            style={{
              width: '100%',
              padding: 12,
              border: `1px solid ${errors.city ? '#dc2626' : '#d1d5db'}`,
              borderRadius: 8,
              fontSize: 16,
            }}
          />
          {errors.city && (
            <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
              {errors.city.message}
            </p>
          )}
        </div>

        <div>
          <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
            Country *
          </label>
          <input
            {...register('country')}
            style={{
              width: '100%',
              padding: 12,
              border: `1px solid ${errors.country ? '#dc2626' : '#d1d5db'}`,
              borderRadius: 8,
              fontSize: 16,
            }}
          />
          {errors.country && (
            <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
              {errors.country.message}
            </p>
          )}
        </div>
      </div>

      <NavigationButtons
        onBack={prevStep}
        onNext={handleSubmit(onSubmit)}
        isValid={isValid}
      />
    </form>
  );
}

// ============= STEP 3: Account =============

function Step3_Account() {
  const { formData, updateFormData, nextStep, prevStep } = useFormContext();

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    watch,
  } = useForm({
    resolver: zodResolver(accountSchema),
    defaultValues: formData.account,
    mode: 'onChange',
  });

  const password = watch('password');

  const onSubmit = (data) => {
    updateFormData('account', data);
    nextStep();
  };

  // Password strength indicator
  const getPasswordStrength = (pwd) => {
    if (!pwd) return { strength: 0, label: 'No password' };
    let strength = 0;
    if (pwd.length >= 8) strength++;
    if (/[A-Z]/.test(pwd)) strength++;
    if (/[a-z]/.test(pwd)) strength++;
    if (/[0-9]/.test(pwd)) strength++;
    if (/[^A-Za-z0-9]/.test(pwd)) strength++;

    const labels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'];
    return { strength, label: labels[strength - 1] || 'Very Weak' };
  };

  const passwordStrength = getPasswordStrength(password);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2 style={{ marginBottom: 24 }}>Account Details</h2>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Username *
        </label>
        <input
          {...register('username')}
          placeholder='Choose a username'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.username ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.username && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.username.message}
          </p>
        )}
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Password *
        </label>
        <input
          type='password'
          {...register('password')}
          placeholder='Create a strong password'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.password ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />

        {/* Password strength indicator */}
        {password && (
          <div style={{ marginTop: 8 }}>
            <div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
              {[1, 2, 3, 4, 5].map((level) => (
                <div
                  key={level}
                  style={{
                    flex: 1,
                    height: 4,
                    backgroundColor:
                      level <= passwordStrength.strength
                        ? passwordStrength.strength < 3
                          ? '#dc2626'
                          : passwordStrength.strength < 4
                            ? '#d97706'
                            : '#059669'
                        : '#e5e7eb',
                    borderRadius: 2,
                  }}
                />
              ))}
            </div>
            <p style={{ fontSize: 12, color: '#6b7280' }}>
              Strength: {passwordStrength.label}
            </p>
          </div>
        )}

        {errors.password && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.password.message}
          </p>
        )}
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Confirm Password *
        </label>
        <input
          type='password'
          {...register('confirmPassword')}
          placeholder='Re-enter password'
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.confirmPassword ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        />
        {errors.confirmPassword && (
          <p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
            {errors.confirmPassword.message}
          </p>
        )}
      </div>

      <NavigationButtons
        onBack={prevStep}
        onNext={handleSubmit(onSubmit)}
        isValid={isValid}
      />
    </form>
  );
}

// ============= STEP 4: Preferences =============

function Step4_Preferences() {
  const { formData, updateFormData, nextStep, prevStep } = useFormContext();

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm({
    resolver: zodResolver(preferencesSchema),
    defaultValues: formData.preferences.language
      ? formData.preferences
      : {
          newsletter: true,
          notifications: 'important',
          language: 'en',
          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        },
    mode: 'onChange',
  });

  const onSubmit = (data) => {
    updateFormData('preferences', data);
    nextStep();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2 style={{ marginBottom: 24 }}>Preferences</h2>

      <div style={{ marginBottom: 24 }}>
        <label
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 12,
            cursor: 'pointer',
            padding: 16,
            backgroundColor: '#f9fafb',
            borderRadius: 8,
          }}
        >
          <input
            type='checkbox'
            {...register('newsletter')}
            style={{ width: 20, height: 20, cursor: 'pointer' }}
          />
          <div>
            <div style={{ fontWeight: 500 }}>Subscribe to newsletter</div>
            <div style={{ fontSize: 14, color: '#6b7280' }}>
              Get updates about new features and promotions
            </div>
          </div>
        </label>
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Notification Preferences *
        </label>
        <select
          {...register('notifications')}
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.notifications ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        >
          <option value='all'>All notifications</option>
          <option value='important'>Important only</option>
          <option value='none'>None</option>
        </select>
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Language *
        </label>
        <select
          {...register('language')}
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.language ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        >
          <option value='en'>English</option>
          <option value='vi'>Tiếng Việt</option>
          <option value='fr'>Français</option>
          <option value='es'>Español</option>
        </select>
      </div>

      <div style={{ marginBottom: 16 }}>
        <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
          Timezone *
        </label>
        <select
          {...register('timezone')}
          style={{
            width: '100%',
            padding: 12,
            border: `1px solid ${errors.timezone ? '#dc2626' : '#d1d5db'}`,
            borderRadius: 8,
            fontSize: 16,
          }}
        >
          <option value='America/New_York'>Eastern Time (ET)</option>
          <option value='America/Chicago'>Central Time (CT)</option>
          <option value='America/Denver'>Mountain Time (MT)</option>
          <option value='America/Los_Angeles'>Pacific Time (PT)</option>
          <option value='Asia/Ho_Chi_Minh'>Vietnam (ICT)</option>
          <option value='Europe/London'>London (GMT)</option>
          <option value='Europe/Paris'>Paris (CET)</option>
        </select>
      </div>

      <NavigationButtons
        onBack={prevStep}
        onNext={handleSubmit(onSubmit)}
        isValid={isValid}
      />
    </form>
  );
}

// Kết quả:
// ✅ Each step independent với own validation
// ✅ Data persisted in context
// ✅ Clean navigation between steps
// ✅ Real-time validation feedback

Demo 3: Review & Submit Step ⭐⭐⭐

jsx
/**
 * Final Step - Review & Submit
 */

function Step5_Review() {
  const { formData, prevStep, goToStep } = useFormContext();
  const [submitting, setSubmitting] = useState(false);
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = async () => {
    setSubmitting(true);

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 2000));

    console.log('📤 Submitting registration:', formData);

    setSubmitting(false);
    setSubmitted(true);
  };

  if (submitted) {
    return (
      <div
        style={{
          textAlign: 'center',
          padding: 60,
          backgroundColor: '#f0fdf4',
          borderRadius: 12,
          border: '2px solid #86efac',
        }}
      >
        <div style={{ fontSize: 64, marginBottom: 24 }}>✅</div>
        <h2 style={{ color: '#059669', marginBottom: 16 }}>
          Registration Successful!
        </h2>
        <p style={{ color: '#166534', fontSize: 18, marginBottom: 24 }}>
          Welcome aboard, {formData.personalInfo.firstName}!
        </p>
        <p style={{ color: '#6b7280', fontSize: 14 }}>
          A confirmation email has been sent to {formData.contact.email}
        </p>
      </div>
    );
  }

  return (
    <div>
      <h2 style={{ marginBottom: 24 }}>Review Your Information</h2>
      <p style={{ color: '#6b7280', marginBottom: 32 }}>
        Please review all information before submitting. Click on any section to
        edit.
      </p>

      {/* Personal Info Section */}
      <div
        style={{
          padding: 20,
          backgroundColor: '#f9fafb',
          borderRadius: 8,
          marginBottom: 16,
          border: '1px solid #e5e7eb',
        }}
      >
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: 16,
          }}
        >
          <h3 style={{ margin: 0 }}>Personal Information</h3>
          <button
            type='button'
            onClick={() => goToStep(0)}
            style={{
              padding: '6px 12px',
              backgroundColor: 'white',
              border: '1px solid #d1d5db',
              borderRadius: 6,
              cursor: 'pointer',
              fontSize: 14,
            }}
          >
            Edit
          </button>
        </div>
        <div
          style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
        >
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Name
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.personalInfo.firstName} {formData.personalInfo.lastName}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Birth Date
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.personalInfo.birthDate}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Gender
            </div>
            <div style={{ fontWeight: 500, textTransform: 'capitalize' }}>
              {formData.personalInfo.gender?.replace('-', ' ')}
            </div>
          </div>
        </div>
      </div>

      {/* Contact Section */}
      <div
        style={{
          padding: 20,
          backgroundColor: '#f9fafb',
          borderRadius: 8,
          marginBottom: 16,
          border: '1px solid #e5e7eb',
        }}
      >
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: 16,
          }}
        >
          <h3 style={{ margin: 0 }}>Contact Information</h3>
          <button
            type='button'
            onClick={() => goToStep(1)}
            style={{
              padding: '6px 12px',
              backgroundColor: 'white',
              border: '1px solid #d1d5db',
              borderRadius: 6,
              cursor: 'pointer',
              fontSize: 14,
            }}
          >
            Edit
          </button>
        </div>
        <div
          style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
        >
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Email
            </div>
            <div style={{ fontWeight: 500 }}>{formData.contact.email}</div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Phone
            </div>
            <div style={{ fontWeight: 500 }}>{formData.contact.phone}</div>
          </div>
          <div style={{ gridColumn: '1 / -1' }}>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Address
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.contact.address}, {formData.contact.city},{' '}
              {formData.contact.country}
            </div>
          </div>
        </div>
      </div>

      {/* Account Section */}
      <div
        style={{
          padding: 20,
          backgroundColor: '#f9fafb',
          borderRadius: 8,
          marginBottom: 16,
          border: '1px solid #e5e7eb',
        }}
      >
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: 16,
          }}
        >
          <h3 style={{ margin: 0 }}>Account Details</h3>
          <button
            type='button'
            onClick={() => goToStep(2)}
            style={{
              padding: '6px 12px',
              backgroundColor: 'white',
              border: '1px solid #d1d5db',
              borderRadius: 6,
              cursor: 'pointer',
              fontSize: 14,
            }}
          >
            Edit
          </button>
        </div>
        <div>
          <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
            Username
          </div>
          <div style={{ fontWeight: 500 }}>{formData.account.username}</div>
        </div>
      </div>

      {/* Preferences Section */}
      <div
        style={{
          padding: 20,
          backgroundColor: '#f9fafb',
          borderRadius: 8,
          marginBottom: 32,
          border: '1px solid #e5e7eb',
        }}
      >
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: 16,
          }}
        >
          <h3 style={{ margin: 0 }}>Preferences</h3>
          <button
            type='button'
            onClick={() => goToStep(3)}
            style={{
              padding: '6px 12px',
              backgroundColor: 'white',
              border: '1px solid #d1d5db',
              borderRadius: 6,
              cursor: 'pointer',
              fontSize: 14,
            }}
          >
            Edit
          </button>
        </div>
        <div
          style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
        >
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Newsletter
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.preferences.newsletter
                ? '✅ Subscribed'
                : '❌ Not subscribed'}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Notifications
            </div>
            <div style={{ fontWeight: 500, textTransform: 'capitalize' }}>
              {formData.preferences.notifications}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Language
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.preferences.language.toUpperCase()}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
              Timezone
            </div>
            <div style={{ fontWeight: 500 }}>
              {formData.preferences.timezone}
            </div>
          </div>
        </div>
      </div>

      {/* Submit Section */}
      <div
        style={{
          padding: 24,
          backgroundColor: '#eff6ff',
          border: '1px solid #3b82f6',
          borderRadius: 8,
          marginBottom: 24,
        }}
      >
        <div style={{ display: 'flex', alignItems: 'start', gap: 12 }}>
          <input
            type='checkbox'
            id='terms'
            defaultChecked
            style={{ marginTop: 4, width: 20, height: 20 }}
          />
          <label
            htmlFor='terms'
            style={{ fontSize: 14, color: '#1e40af' }}
          >
            I agree to the Terms of Service and Privacy Policy. I understand
            that my information will be processed according to these terms.
          </label>
        </div>
      </div>

      <NavigationButtons
        onBack={prevStep}
        onNext={handleSubmit}
        isLastStep={true}
        isValid={!submitting}
      />

      {submitting && (
        <div
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            backgroundColor: 'rgba(0,0,0,0.5)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            zIndex: 9999,
          }}
        >
          <div
            style={{
              padding: 40,
              backgroundColor: 'white',
              borderRadius: 12,
              textAlign: 'center',
            }}
          >
            <div style={{ fontSize: 48, marginBottom: 16 }}>⏳</div>
            <div style={{ fontSize: 18, fontWeight: 500 }}>
              Submitting your registration...
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ============= MAIN WIZARD =============

function RegistrationWizard() {
  const { currentStep } = useFormContext();

  const steps = [
    <Step1_PersonalInfo />,
    <Step2_Contact />,
    <Step3_Account />,
    <Step4_Preferences />,
    <Step5_Review />,
  ];

  return (
    <div
      style={{
        maxWidth: 800,
        margin: '40px auto',
        padding: 40,
        backgroundColor: 'white',
        borderRadius: 12,
        boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
      }}
    >
      <h1 style={{ marginBottom: 32, textAlign: 'center' }}>
        Create Your Account
      </h1>

      <ProgressBar
        currentStep={currentStep}
        totalSteps={5}
      />

      {steps[currentStep]}
    </div>
  );
}

// ============= APP =============

function App() {
  return (
    <FormProvider>
      <div
        style={{
          backgroundColor: '#f9fafb',
          minHeight: '100vh',
          padding: '20px 0',
        }}
      >
        <RegistrationWizard />
      </div>
    </FormProvider>
  );
}

// Kết quả:
// ✅ Complete multi-step wizard
// ✅ All steps validated independently
// ✅ Data persisted across steps
// ✅ Review step với edit capabilities
// ✅ Smooth submission flow
// ✅ Success confirmation

🔨 PHẦN 3: ENHANCEMENTS (30 phút)

Enhancement 1: LocalStorage Persistence

💡 Solution
jsx
/**
 * Auto-save to localStorage
 */

function FormProvider({ children }) {
  const [formData, setFormData] = useState(() => {
    // Load from localStorage on init
    const saved = localStorage.getItem('registrationFormData');
    return saved
      ? JSON.parse(saved)
      : {
          personalInfo: {},
          contact: {},
          account: {},
          preferences: {},
        };
  });

  const [currentStep, setCurrentStep] = useState(() => {
    const savedStep = localStorage.getItem('registrationStep');
    return savedStep ? parseInt(savedStep) : 0;
  });

  const updateFormData = (step, data) => {
    setFormData((prev) => {
      const updated = {
        ...prev,
        [step]: { ...prev[step], ...data },
      };

      // Save to localStorage
      localStorage.setItem('registrationFormData', JSON.stringify(updated));

      return updated;
    });
  };

  const nextStep = () => {
    setCurrentStep((prev) => {
      const next = Math.min(prev + 1, 4);
      localStorage.setItem('registrationStep', next.toString());
      return next;
    });
  };

  const prevStep = () => {
    setCurrentStep((prev) => {
      const next = Math.max(prev - 1, 0);
      localStorage.setItem('registrationStep', next.toString());
      return next;
    });
  };

  const clearForm = () => {
    localStorage.removeItem('registrationFormData');
    localStorage.removeItem('registrationStep');
    setFormData({
      personalInfo: {},
      contact: {},
      account: {},
      preferences: {},
    });
    setCurrentStep(0);
  };

  return (
    <FormContext.Provider
      value={{
        formData,
        updateFormData,
        currentStep,
        nextStep,
        prevStep,
        goToStep: setCurrentStep,
        clearForm,
      }}
    >
      {children}
    </FormContext.Provider>
  );
}

// Kết quả:
// ✅ Form data persisted across browser refreshes
// ✅ User can continue where they left off
// ✅ Clear function to reset everything

Enhancement 2: File Upload (Profile Picture)

💡 Solution
jsx
/**
 * Add profile picture upload to Step 1
 */

function Step1_PersonalInfo() {
  const { formData, updateFormData, nextStep } = useFormContext();
  const [preview, setPreview] = useState(
    formData.personalInfo.profilePicture || null,
  );

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    setValue,
  } = useForm({
    resolver: zodResolver(personalInfoSchema),
    defaultValues: formData.personalInfo,
    mode: 'onChange',
  });

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      // Validate file size (max 5MB)
      if (file.size > 5 * 1024 * 1024) {
        alert('File size must be less than 5MB');
        return;
      }

      // Validate file type
      if (!file.type.startsWith('image/')) {
        alert('Please upload an image file');
        return;
      }

      // Create preview
      const reader = new FileReader();
      reader.onloadend = () => {
        setPreview(reader.result);
        setValue('profilePicture', reader.result);
      };
      reader.readAsDataURL(file);
    }
  };

  const onSubmit = (data) => {
    updateFormData('personalInfo', data);
    nextStep();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2 style={{ marginBottom: 24 }}>Personal Information</h2>

      {/* Profile Picture Upload */}
      <div style={{ marginBottom: 24, textAlign: 'center' }}>
        <div
          style={{
            width: 120,
            height: 120,
            borderRadius: '50%',
            backgroundColor: '#f3f4f6',
            margin: '0 auto 16px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            overflow: 'hidden',
            border: '3px solid #e5e7eb',
          }}
        >
          {preview ? (
            <img
              src={preview}
              alt='Profile'
              style={{ width: '100%', height: '100%', objectFit: 'cover' }}
            />
          ) : (
            <span style={{ fontSize: 48, color: '#9ca3af' }}>👤</span>
          )}
        </div>

        <label
          style={{
            display: 'inline-block',
            padding: '8px 16px',
            backgroundColor: '#3b82f6',
            color: 'white',
            borderRadius: 8,
            cursor: 'pointer',
            fontSize: 14,
          }}
        >
          Upload Photo
          <input
            type='file'
            accept='image/*'
            onChange={handleFileChange}
            style={{ display: 'none' }}
          />
        </label>
        <p style={{ fontSize: 12, color: '#6b7280', marginTop: 8 }}>
          Max 5MB • JPG, PNG, GIF
        </p>
      </div>

      {/* Rest of the form... */}
    </form>
  );
}

// Kết quả:
// ✅ File upload với preview
// ✅ File size validation
// ✅ File type validation
// ✅ Base64 encoding for storage

📊 PHẦN 4: PROJECT CHECKLIST (15 phút)

Production Checklist

Core Functionality:

  • [x] Multi-step wizard (5 steps)
  • [x] React Hook Form integration
  • [x] Zod validation per step
  • [x] Context state management
  • [x] Progress indicator
  • [x] Navigation (Next/Back)
  • [x] Review & edit capability
  • [x] Form submission

User Experience:

  • [x] Real-time validation feedback
  • [x] Error messages clear
  • [x] Loading states during submission
  • [x] Success confirmation
  • [x] Smooth transitions
  • [x] Mobile responsive

Data Management:

  • [x] State persisted across steps
  • [x] LocalStorage persistence (optional)
  • [x] Form reset capability
  • [x] Data validation before submit

Code Quality:

  • [x] Components well-organized
  • [x] Validation schemas separated
  • [x] Reusable components
  • [x] Clean code structure
  • [x] Comments where needed

✅ PHẦN 5: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

  • [ ] Tôi hiểu wizard pattern và benefits
  • [ ] Tôi biết cách structure multi-step forms
  • [ ] Tôi biết combine React Hook Form + Zod + Context
  • [ ] Tôi hiểu cách validate từng step independently
  • [ ] Tôi biết persist data across steps
  • [ ] Tôi biết implement navigation between steps
  • [ ] Tôi biết build review & edit functionality
  • [ ] Tôi hiểu form submission flow
  • [ ] Tôi biết handle file uploads trong forms
  • [ ] Tôi có thể build complete registration flow

Code Review Checklist

Form Structure:

  • [ ] Steps logically organized
  • [ ] Each step focused và không quá dài
  • [ ] Clear progress indication
  • [ ] Easy navigation

Validation:

  • [ ] Zod schemas properly defined
  • [ ] Validation per step works
  • [ ] Error messages clear
  • [ ] Real-time feedback

State Management:

  • [ ] Context setup correctly
  • [ ] State updates properly
  • [ ] No unnecessary re-renders
  • [ ] Data persisted correctly

User Experience:

  • [ ] Smooth transitions
  • [ ] Loading states
  • [ ] Success feedback
  • [ ] Error handling
  • [ ] Mobile friendly

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Thêm validation rules:

  • Async username availability check
  • Password strength requirements customizable
  • Email domain validation (corporate emails only)
  • Phone number country code validation

Nâng cao (60 phút)

Enhance registration flow:

  • Add "Save as draft" functionality
  • Implement "Resume later" with email link
  • Add progress auto-save every 30s
  • Implement form analytics (track step completion rates)
  • Add A/B testing capability for different flows

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Hook Form - Multi-step Forms
  2. Zod - Schema Composition

Đọc thêm

  1. UX Best Practices for Multi-step Forms
  2. Form Wizard Patterns

🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền

  • Ngày 41: React Hook Form Basics
  • Ngày 42: React Hook Form Advanced
  • Ngày 43: Zod Validation Schemas
  • Ngày 44: Multi-step Forms Theory
  • Ngày 36-38: Context API

Hướng tới

  • Ngày 46: React 18 Features Introduction
  • Ngày 47-50: Concurrent Features
  • Testing: Testing multi-step forms

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Analytics Integration:

jsx
// Track step completion
const nextStep = () => {
  analytics.track('form_step_completed', {
    step: currentStep,
    stepName: stepNames[currentStep],
    timeSpent: Date.now() - stepStartTime,
  });

  setCurrentStep((prev) => prev + 1);
};

2. A/B Testing:

jsx
// Test different flows
const flowVariant = useABTest('registration-flow');

const steps =
  flowVariant === 'A'
    ? [Step1, Step2, Step3, Step4, Step5] // Original
    : [Step1Combined, Step2, Step3Review]; // Shorter

3. Abandoned Cart Recovery:

jsx
// Email reminder for incomplete registrations
useEffect(() => {
  if (currentStep > 0 && currentStep < 4) {
    // User started but didn't finish
    const saveProgress = async () => {
      await fetch('/api/save-progress', {
        method: 'POST',
        body: JSON.stringify({
          email: formData.contact?.email,
          step: currentStep,
          data: formData,
        }),
      });
    };

    const timer = setTimeout(saveProgress, 5 * 60 * 1000); // 5 min
    return () => clearTimeout(timer);
  }
}, [currentStep]);

Câu Hỏi Phỏng Vấn

Junior:

  1. Multi-step form có lợi ích gì so với single-page form?
  2. Làm sao persist data across steps?
  3. Validation từng step hoạt động như thế nào?

Mid:

  1. So sánh approaches khác nhau cho multi-step forms
  2. Optimize performance trong wizard forms
  3. Handle errors trong multi-step submissions

Senior:

  1. Design system architecture cho complex form flows
  2. A/B testing strategies cho form optimization
  3. Analytics và conversion tracking trong forms

War Stories

Story: The 20-Step Monster

"Client yêu cầu onboarding form với 20 steps. Users abandoned massively (85% drop-off).

Solution:

  • Reduced to 5 essential steps
  • Made 10 steps optional (edit profile later)
  • Removed 5 steps entirely (not needed)
  • Added progress save every step

Result: Completion rate từ 15% → 65%

Lesson: Less is more. Only ask what you NEED, not what you WANT."


🎯 PREVIEW NGÀY 46

Ngày mai bắt đầu Phase 5: Modern React Features!

Chúng ta sẽ học về:

  • React 18 Concurrent Rendering
  • Automatic Batching
  • Performance benefits
  • Breaking changes to know

Chuẩn bị khám phá những tính năng mới nhất của React! 🚀


🎉 CHÚC MỪNG! Bạn đã hoàn thành Phase 4: Advanced Patterns!

Bạn đã master: ✅ Context API ✅ Component Patterns ✅ React Hook Form ✅ Zod Validation ✅ Multi-step Forms ✅ Production-ready Registration Flow

40+ ngày React journey đã hoàn thành! Bạn đã sẵn sàng cho Modern React Features! 💪

Personal tech knowledge base