Skip to content

📅 NGÀY 13: Forms với State

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

Sau bài học này, bạn sẽ:

  • [ ] Phân biệt Controlled vs Uncontrolled components và biết khi nào dùng approach nào
  • [ ] Xử lý multiple form inputs một cách hiệu quả với state structure hợp lý
  • [ ] Implement form validation realtime và on-submit với useState
  • [ ] Handle form submission đúng cách (prevent default, clear form, error handling)
  • [ ] Optimize form performance với các patterns từ Ngày 12

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

Trả lời 3 câu hỏi sau để kích hoạt kiến thức từ Ngày 11-12:

  1. Câu 1: Code này có vấn đề gì?
jsx
const [formData, setFormData] = useState({ email: '' });

const handleChange = (e) => {
  formData.email = e.target.value; // ❓
  setFormData(formData);
};
  1. Câu 2: Khi nào nên dùng functional updates setState(prev => ...)?

  2. Câu 3: Tại sao không nên store isEmailValid trong state nếu có thể validate từ email?

💡 Xem đáp án
  1. Mutation! Phải dùng immutable update: setFormData(prev => ({...prev, email: e.target.value}))
  2. Khi update dựa trên previous value, trong async operations, hoặc multiple updates
  3. Derived state anti-pattern! isEmailValid có thể compute từ email → không cần store riêng

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Forms là phần KHẮC NGHIỆT NHẤT của React cho beginners. Tại sao?

jsx
// HTML form truyền thống
<form>
  <input name="email" /> {/* Browser tự quản lý value */}
  <button type="submit">Submit</button>
</form>

// React form - PHẢI quản lý state
<form>
  <input
    value={email}              // ❓ Lấy từ đâu?
    onChange={handleChange}    // ❓ Làm gì?
  />
  <button type="submit">Submit</button>
</form>

Challenges:

  • 🤔 Form state nên ở đâu?
  • 🤔 Làm sao sync input value với state?
  • 🤔 Validate khi nào? (onChange, onBlur, onSubmit?)
  • 🤔 Handle errors thế nào?
  • 🤔 Multiple inputs → nhiều states hay một object?

Hôm nay sẽ giải quyết TẤT CẢ những câu hỏi này!


1.2 Giải Pháp: Controlled Components

Core Concept:

┌─────────────────────────────────────────┐
│        CONTROLLED COMPONENT             │
├─────────────────────────────────────────┤
│                                         │
│  React State = Single Source of Truth   │
│                                         │
│  User types → onChange → setState       │
│       ↓                                 │
│  State updates → Re-render              │
│       ↓                                 │
│  Input value = state value              │
│                                         │
│  ┌──────────┐                           │
│  │  State   │ ←───────────┐             │
│  │  email   │             │             │
│  └────┬─────┘             │             │
│       │                   │             │
│       ↓                   │             │
│  <input                   │             │
│    value={email}          │             │
│    onChange={e => ────────┘             │
│      setEmail(e.target.value)           │
│    }                                    │
│  />                                     │
└─────────────────────────────────────────┘

React controls the input, not the browser!


1.3 Mental Model

Hãy tưởng tượng form như dashboard với live updates:

Form truyền thống (Uncontrolled):
Người dùng nhập → Trình duyệt lưu → Submit → Đọc toàn bộ một lần
[Giống như viết trên giấy → nộp khi xong]

Form React (Controlled):
Người dùng nhập → State React cập nhật → UI cập nhật ngay lập tức
[Giống như bảng điều khiển trực tiếp → thấy thay đổi theo thời gian thực]

Analogy:

  • Uncontrolled: Gửi thư qua bưu điện (submit mới biết nội dung)
  • Controlled: Nhắn tin realtime (thấy từng ký tự ngay lập tức)

1.4 Hiểu Lầm Phổ Biến

Myth 1: "Controlled components phức tạp hơn, không dùng được không?"
Truth: 95% cases nên dùng controlled. Uncontrolled chỉ cho edge cases.

Myth 2: "Mỗi input cần 1 state riêng"
Truth: Nên group related inputs vào 1 object (Ngày 12!)

Myth 3: "Validate chỉ khi submit"
Truth: Best UX là realtime validation + final check on submit

Myth 4: "Forms trong React khó hơn HTML"
Truth: Khó hơn ban đầu, nhưng powerful hơn NHIỀU (validation, dynamic fields, etc.)


💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Controlled vs Uncontrolled - Pattern Cơ Bản ⭐

jsx
// ❌ Browser controls the input value
function UncontrolledForm() {
  const handleSubmit = (e) => {
    e.preventDefault();

    // ❌ Phải query DOM để lấy value
    const email = e.target.elements.email.value;
    const password = e.target.elements.password.value;

    console.log('Submitted:', { email, password });

    // ❌ Không biết value trước khi submit
    // ❌ Không validate được realtime
    // ❌ Không disable submit button khi invalid
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name='email'
        type='email'
        // ❌ Không có value prop → Browser controls
      />
      <input
        name='password'
        type='password'
      />
      <button type='submit'>Submit</button>
    </form>
  );
}

Problems:

  • ❌ Không biết user đang gõ gì
  • ❌ Không validate được realtime
  • ❌ Không disable submit khi invalid
  • ❌ Khó dynamic UI (show/hide fields)
  • ❌ Khó auto-format (phone number, credit card)

jsx
// ✅ React controls the input value
function ControlledForm() {
  // ✅ State = Single Source of Truth
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();

    // ✅ Đã có value từ state
    console.log('Submitted:', { email, password });

    // ✅ Clear form sau khi submit
    setEmail('');
    setPassword('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Email:</label>
        <input
          type='email'
          value={email} // ✅ React controls value
          onChange={(e) => setEmail(e.target.value)}
        />
        <p>You typed: {email}</p> {/* ✅ Can use state anywhere */}
      </div>

      <div>
        <label>Password:</label>
        <input
          type='password'
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <p>Length: {password.length}</p> {/* ✅ Realtime info */}
      </div>

      <button
        type='submit'
        disabled={!email || password.length < 6} // ✅ Smart button!
      >
        Submit
      </button>
    </form>
  );
}

Benefits:

  • ✅ Biết user đang gõ gì (realtime)
  • ✅ Validate ngay khi gõ
  • ✅ Disable submit khi invalid
  • ✅ Hiển thị UI hữu ích (độ mạnh mật khẩu, định dạng email)
  • ✅ Dễ dàng biến đổi dữ liệu nhập (viết hoa, định dạng, v.v.)

Demo 2: Multiple Inputs với Object State - Kịch Bản Thực Tế ⭐⭐

❌ CÁCH SAI: Mỗi Input 1 State Riêng

jsx
// ❌ Code dài dòng, khó maintain
function SignupFormBad() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [phone, setPhone] = useState('');
  const [address, setAddress] = useState('');
  const [city, setCity] = useState('');
  const [country, setCountry] = useState('');
  // 😱 9 states cho 1 form!

  const handleSubmit = (e) => {
    e.preventDefault();

    // ❌ Phải list tất cả fields
    const formData = {
      firstName,
      lastName,
      email,
      password,
      confirmPassword,
      phone,
      address,
      city,
      country,
    };

    console.log(formData);

    // ❌ Reset form = 9 dòng code!
    setFirstName('');
    setLastName('');
    setEmail('');
    setPassword('');
    setConfirmPassword('');
    setPhone('');
    setAddress('');
    setCity('');
    setCountry('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />
      <input
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
      />
      {/* ...7 inputs nữa với 7 onChange handlers! */}
    </form>
  );
}

Problems:

  • 😱 Too many states
  • 😱 Too many onChange handlers
  • 😱 Reset form phức tạp
  • 😱 Hard to validate related fields
  • 😱 Can't easily pass to API

✅ CÁCH ĐÚNG: Single Object State

jsx
// ✅ Clean, maintainable, scalable
function SignupFormGood() {
  // ✅ Group related data (apply Ngày 12!)
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    phone: '',
    address: '',
    city: '',
    country: '',
  });

  // ✅ Generic handler cho tất cả inputs
  const handleChange = (e) => {
    const { name, value } = e.target;

    setFormData((prev) => ({
      ...prev, // Keep old values
      [name]: value, // Update changed field
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // ✅ formData đã sẵn sàng để gửi API
    console.log('Submitting:', formData);

    // ✅ Reset form = 1 dòng!
    setFormData({
      firstName: '',
      lastName: '',
      email: '',
      password: '',
      confirmPassword: '',
      phone: '',
      address: '',
      city: '',
      country: '',
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>First Name:</label>
        <input
          name='firstName' // ⚠️ IMPORTANT: name attribute!
          value={formData.firstName}
          onChange={handleChange} // ✅ Reuse same handler
        />
      </div>

      <div>
        <label>Last Name:</label>
        <input
          name='lastName'
          value={formData.lastName}
          onChange={handleChange}
        />
      </div>

      <div>
        <label>Email:</label>
        <input
          name='email'
          type='email'
          value={formData.email}
          onChange={handleChange}
        />
      </div>

      <div>
        <label>Password:</label>
        <input
          name='password'
          type='password'
          value={formData.password}
          onChange={handleChange}
        />
      </div>

      <div>
        <label>Confirm Password:</label>
        <input
          name='confirmPassword'
          type='password'
          value={formData.confirmPassword}
          onChange={handleChange}
        />
      </div>

      {/* ...more inputs... */}

      <button type='submit'>Sign Up</button>

      {/* Debug view */}
      <details>
        <summary>Form Data</summary>
        <pre>{JSON.stringify(formData, null, 2)}</pre>
      </details>
    </form>
  );
}

🔥 KEY PATTERN:

jsx
// Magic pattern: 1 handler for ALL inputs!
const handleChange = (e) => {
  const { name, value } = e.target; // Destructure
  setFormData((prev) => ({
    ...prev, // Immutable update (Ngày 12!)
    [name]: value, // Computed property name
  }));
};

// Each input needs `name` attribute matching state key
<input
  name='email' // Must match formData.email
  value={formData.email}
  onChange={handleChange}
/>;

Demo 3: Validation & Error Handling - Edge Cases ⭐⭐⭐

jsx
// ✅ Production-ready form with validation
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    age: '',
  });

  // ✅ Separate state for errors
  const [errors, setErrors] = useState({});

  // ✅ Track which fields user has touched
  const [touched, setTouched] = useState({});

  // ✅ Validation rules (pure functions)
  const validators = {
    username: (value) => {
      if (!value) return 'Username is required';
      if (value.length < 3) return 'Username must be at least 3 characters';
      if (!/^[a-zA-Z0-9_]+$/.test(value))
        return 'Only letters, numbers, underscore';
      return ''; // No error
    },

    email: (value) => {
      if (!value) return 'Email is required';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
        return 'Invalid email format';
      return '';
    },

    password: (value) => {
      if (!value) return 'Password is required';
      if (value.length < 8) return 'Password must be at least 8 characters';
      if (!/[A-Z]/.test(value)) return 'Password must contain uppercase letter';
      if (!/[0-9]/.test(value)) return 'Password must contain number';
      return '';
    },

    confirmPassword: (value) => {
      if (!value) return 'Please confirm password';
      if (value !== formData.password) return 'Passwords do not match';
      return '';
    },

    age: (value) => {
      if (!value) return 'Age is required';
      const num = Number(value);
      if (isNaN(num)) return 'Age must be a number';
      if (num < 13) return 'Must be at least 13 years old';
      if (num > 120) return 'Invalid age';
      return '';
    },
  };

  // ✅ Validate single field
  const validateField = (name, value) => {
    if (validators[name]) {
      return validators[name](value);
    }
    return '';
  };

  // ✅ Validate all fields
  const validateAll = () => {
    const newErrors = {};

    Object.keys(formData).forEach((field) => {
      const error = validateField(field, formData[field]);
      if (error) {
        newErrors[field] = error;
      }
    });

    return newErrors;
  };

  // ✅ Handle input change with validation
  const handleChange = (e) => {
    const { name, value } = e.target;

    // Update form data
    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));

    // Clear error for this field when user types
    if (errors[name]) {
      setErrors((prev) => ({
        ...prev,
        [name]: '',
      }));
    }

    // Validate confirmPassword when password changes
    if (name === 'password' && formData.confirmPassword) {
      const confirmError = validators.confirmPassword(formData.confirmPassword);
      setErrors((prev) => ({
        ...prev,
        confirmPassword: confirmError,
      }));
    }
  };

  // ✅ Handle blur (when user leaves field)
  const handleBlur = (e) => {
    const { name, value } = e.target;

    // Mark field as touched
    setTouched((prev) => ({
      ...prev,
      [name]: true,
    }));

    // Validate field
    const error = validateField(name, value);
    if (error) {
      setErrors((prev) => ({
        ...prev,
        [name]: error,
      }));
    }
  };

  // ✅ Handle submit
  const handleSubmit = (e) => {
    e.preventDefault();

    // Mark all fields as touched
    const allTouched = Object.keys(formData).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {});
    setTouched(allTouched);

    // Validate all
    const validationErrors = validateAll();

    if (Object.keys(validationErrors).length > 0) {
      // Has errors
      setErrors(validationErrors);
      console.log('Form has errors:', validationErrors);
      return;
    }

    // ✅ Form is valid!
    console.log('Form submitted successfully:', formData);

    // Clear form
    setFormData({
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      age: '',
    });
    setErrors({});
    setTouched({});
  };

  // ✅ Derived: Is form valid?
  const isFormValid =
    Object.keys(validateAll()).length === 0 &&
    Object.keys(formData).every((key) => formData[key]);

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}
    >
      <h2>Registration Form</h2>

      {/* Username */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          Username:
        </label>
        <input
          name='username'
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '8px',
            border:
              errors.username && touched.username
                ? '2px solid red'
                : '1px solid #ccc',
          }}
        />
        {touched.username && errors.username && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.username}
          </p>
        )}
      </div>

      {/* Email */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>Email:</label>
        <input
          name='email'
          type='email'
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '8px',
            border:
              errors.email && touched.email
                ? '2px solid red'
                : '1px solid #ccc',
          }}
        />
        {touched.email && errors.email && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.email}
          </p>
        )}
      </div>

      {/* Password */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          Password:
        </label>
        <input
          name='password'
          type='password'
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '8px',
            border:
              errors.password && touched.password
                ? '2px solid red'
                : '1px solid #ccc',
          }}
        />
        {touched.password && errors.password && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.password}
          </p>
        )}
        {/* Password strength indicator */}
        {formData.password && (
          <div style={{ marginTop: '5px', fontSize: '0.85em' }}>
            <span
              style={{
                color: formData.password.length >= 8 ? 'green' : 'gray',
              }}
            >
              ✓ 8+ characters
            </span>
            {' | '}
            <span
              style={{
                color: /[A-Z]/.test(formData.password) ? 'green' : 'gray',
              }}
            >
              ✓ Uppercase
            </span>
            {' | '}
            <span
              style={{
                color: /[0-9]/.test(formData.password) ? 'green' : 'gray',
              }}
            >
              ✓ Number
            </span>
          </div>
        )}
      </div>

      {/* Confirm Password */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          Confirm Password:
        </label>
        <input
          name='confirmPassword'
          type='password'
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '8px',
            border:
              errors.confirmPassword && touched.confirmPassword
                ? '2px solid red'
                : '1px solid #ccc',
          }}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.confirmPassword}
          </p>
        )}
      </div>

      {/* Age */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>Age:</label>
        <input
          name='age'
          type='number'
          value={formData.age}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '8px',
            border:
              errors.age && touched.age ? '2px solid red' : '1px solid #ccc',
          }}
        />
        {touched.age && errors.age && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.age}
          </p>
        )}
      </div>

      {/* Submit Button */}
      <button
        type='submit'
        disabled={!isFormValid}
        style={{
          width: '100%',
          padding: '12px',
          background: isFormValid ? '#4CAF50' : '#ccc',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: isFormValid ? 'pointer' : 'not-allowed',
          fontSize: '16px',
        }}
      >
        Register
      </button>

      {/* Debug */}
      <details style={{ marginTop: '20px' }}>
        <summary>Debug Info</summary>
        <div style={{ fontSize: '0.85em' }}>
          <p>
            <strong>Form Data:</strong>
          </p>
          <pre>{JSON.stringify(formData, null, 2)}</pre>
          <p>
            <strong>Errors:</strong>
          </p>
          <pre>{JSON.stringify(errors, null, 2)}</pre>
          <p>
            <strong>Touched:</strong>
          </p>
          <pre>{JSON.stringify(touched, null, 2)}</pre>
          <p>
            <strong>Is Valid:</strong> {isFormValid ? 'Yes' : 'No'}
          </p>
        </div>
      </details>
    </form>
  );
}

🔥 KEY PATTERNS:

  1. Tách riêng state lỗi:
jsx
const [formData, setFormData] = useState({...});
const [errors, setErrors] = useState({});      // ✅ Tách riêng!
const [touched, setTouched] = useState({});    // ✅ Theo dõi field đã chạm
  1. Thời điểm validate:
jsx
onChange  → Xoá lỗi (UX tốt - không hiện lỗi khi đang gõ)
onBlur    → Validate field (hiện lỗi khi người dùng rời field)
onSubmit  → Validate tất cả (kiểm tra cuối cùng)
  1. Validate giữa các field:
jsx
// Khi password thay đổi, validate lại confirmPassword
if (name === 'password' && formData.confirmPassword) {
  validateField('confirmPassword', formData.confirmPassword);
}

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Exercise 1: Basic Login Form (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Tạo controlled login form cơ bản
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useEffect, useRef, external libraries
 *
 * Requirements:
 * 1. 2 inputs: email và password (controlled)
 * 2. Submit button disabled khi email rỗng HOẶC password < 6 ký tự
 * 3. onSubmit: console.log form data và clear form
 * 4. Hiển thị "Password length: X" khi user gõ password
 *
 * 💡 Gợi ý:
 * - Dùng 2 separate states hoặc 1 object state (bạn chọn!)
 * - e.preventDefault() trong handleSubmit
 */

// ❌ Starter code (có bugs):
function LoginForm() {
  // TODO: Add state

  const handleSubmit = (e) => {
    // TODO: Implement
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Email:</label>
        <input type='email' />
        {/* TODO: Make controlled */}
      </div>

      <div>
        <label>Password:</label>
        <input type='password' />
        {/* TODO: Make controlled */}
        {/* TODO: Show length */}
      </div>

      <button type='submit'>
        {/* TODO: Disable when invalid */}
        Login
      </button>
    </form>
  );
}

// ✅ NHIỆM VỤ CỦA BẠN:
// TODO: Implement controlled inputs
// TODO: Add state management
// TODO: Implement validation
// TODO: Handle submit
💡 Solution
jsx
function LoginForm() {
  // Option 1: Separate states (simple form)
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // Option 2: Object state (recommended for larger forms)
  // const [formData, setFormData] = useState({ email: '', password: '' });

  const handleSubmit = (e) => {
    e.preventDefault(); // ✅ Prevent page reload

    console.log('Login submitted:', { email, password });

    // Clear form
    setEmail('');
    setPassword('');
  };

  // Validation logic
  const isValid = email.trim() !== '' && password.length >= 6;

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '400px', padding: '20px' }}
    >
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>Email:</label>
        <input
          type='email'
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder='you@example.com'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          Password:
        </label>
        <input
          type='password'
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder='Min 6 characters'
          style={{ width: '100%', padding: '8px' }}
        />
        {password && (
          <p
            style={{
              fontSize: '0.85em',
              color: password.length >= 6 ? 'green' : 'red',
            }}
          >
            Password length: {password.length}{' '}
            {password.length >= 6 ? '✓' : '(min 6)'}
          </p>
        )}
      </div>

      <button
        type='submit'
        disabled={!isValid}
        style={{
          width: '100%',
          padding: '10px',
          background: isValid ? '#007bff' : '#ccc',
          color: 'white',
          border: 'none',
          cursor: isValid ? 'pointer' : 'not-allowed',
        }}
      >
        Login
      </button>

      {/* Debug */}
      <div style={{ marginTop: '20px', fontSize: '0.85em', color: '#666' }}>
        <p>Email: {email || '(empty)'}</p>
        <p>Password: {'*'.repeat(password.length)}</p>
        <p>Valid: {isValid ? 'Yes ✓' : 'No ✗'}</p>
      </div>
    </form>
  );
}

⭐⭐ Exercise 2: Contact Form with Multiple Inputs (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Handle multiple inputs efficiently
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Contact form với nhiều fields
 *
 * 🤔 PHÂN TÍCH:
 * Approach A: Mỗi field 1 state riêng
 * Pros: Đơn giản, rõ ràng
 * Cons: Nhiều states, nhiều onChange handlers
 *
 * Approach B: Object state + generic handler
 * Pros: Scalable, ít code hơn, easy to extend
 * Cons: Phức tạp hơn chút (nhưng đáng!)
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 */

// Requirements:
const FIELDS = {
  name: { label: 'Full Name', type: 'text', required: true },
  email: { label: 'Email', type: 'email', required: true },
  phone: { label: 'Phone', type: 'tel', required: false },
  subject: { label: 'Subject', type: 'text', required: true },
  message: { label: 'Message', type: 'textarea', required: true },
};

// ✅ NHIỆM VỤ CỦA BẠN:

function ContactForm() {
  // TODO: Design state structure (justify your choice!)

  // TODO: Implement generic handleChange
  const handleChange = (e) => {
    // Hint: const { name, value } = e.target;
  };

  // TODO: Implement handleSubmit
  const handleSubmit = (e) => {
    e.preventDefault();
    // 1. Validate required fields
    // 2. Console.log if valid
    // 3. Clear form
  };

  // TODO: Validation - check all required fields filled
  const isValid = false; // Replace with actual logic

  return (
    <form onSubmit={handleSubmit}>
      {/* TODO: Render inputs dynamically or manually */}

      <button
        type='submit'
        disabled={!isValid}
      >
        Send Message
      </button>
    </form>
  );
}

// 📝 Document your decision:
/**
 * State Structure Decision:
 *
 * Chosen Approach: [A or B]
 *
 * Rationale:
 * - [Why this approach?]
 *
 * Trade-offs:
 * - [What are you giving up?]
 */
💡 Solution
jsx
/**
 * State Structure Decision:
 *
 * Chosen Approach: B - Object state + generic handler
 *
 * Rationale:
 * - 5 fields → would need 5 states + 5 handlers in Approach A
 * - Generic handler scales to any number of fields
 * - Easy to add/remove fields in future
 * - Form data already in object shape for API
 *
 * Trade-offs:
 * - Slightly more complex initially
 * - Need to understand computed property names
 * - Worth it for maintainability!
 */

const FIELDS = {
  name: { label: 'Full Name', type: 'text', required: true },
  email: { label: 'Email', type: 'email', required: true },
  phone: { label: 'Phone', type: 'tel', required: false },
  subject: { label: 'Subject', type: 'text', required: true },
  message: { label: 'Message', type: 'textarea', required: true },
};

function ContactForm() {
  // ✅ Single object state
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: '',
    subject: '',
    message: '',
  });

  // ✅ Generic handler for all inputs
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  // ✅ Validate required fields
  const validateForm = () => {
    // Check all required fields are filled
    return Object.keys(FIELDS).every((fieldName) => {
      const field = FIELDS[fieldName];
      if (field.required) {
        return formData[fieldName].trim() !== '';
      }
      return true; // Optional fields always pass
    });
  };

  const isValid = validateForm();

  // ✅ Handle submit
  const handleSubmit = (e) => {
    e.preventDefault();

    if (!isValid) {
      console.log('Form is invalid!');
      return;
    }

    console.log('Contact form submitted:', formData);

    // Clear form
    setFormData({
      name: '',
      email: '',
      phone: '',
      subject: '',
      message: '',
    });

    alert('Message sent successfully!');
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}
    >
      <h2>Contact Us</h2>

      {/* Render fields dynamically */}
      {Object.keys(FIELDS).map((fieldName) => {
        const field = FIELDS[fieldName];
        const isTextarea = field.type === 'textarea';

        return (
          <div
            key={fieldName}
            style={{ marginBottom: '15px' }}
          >
            <label
              style={{
                display: 'block',
                marginBottom: '5px',
                fontWeight: 'bold',
              }}
            >
              {field.label}
              {field.required && <span style={{ color: 'red' }}> *</span>}
            </label>

            {isTextarea ? (
              <textarea
                name={fieldName}
                value={formData[fieldName]}
                onChange={handleChange}
                rows={5}
                style={{ width: '100%', padding: '8px', fontFamily: 'inherit' }}
              />
            ) : (
              <input
                type={field.type}
                name={fieldName}
                value={formData[fieldName]}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
            )}

            {/* Show character count for message */}
            {fieldName === 'message' && formData.message && (
              <p
                style={{
                  fontSize: '0.85em',
                  color: '#666',
                  margin: '5px 0 0 0',
                }}
              >
                {formData.message.length} characters
              </p>
            )}
          </div>
        );
      })}

      <button
        type='submit'
        disabled={!isValid}
        style={{
          width: '100%',
          padding: '12px',
          background: isValid ? '#28a745' : '#ccc',
          color: 'white',
          border: 'none',
          fontSize: '16px',
          cursor: isValid ? 'pointer' : 'not-allowed',
          borderRadius: '4px',
        }}
      >
        Send Message
      </button>

      {/* Missing fields indicator */}
      {!isValid && (
        <p style={{ color: 'red', marginTop: '10px', fontSize: '0.9em' }}>
          Please fill in all required fields (*)
        </p>
      )}

      {/* Debug */}
      <details style={{ marginTop: '20px' }}>
        <summary>Debug: Form Data</summary>
        <pre
          style={{ background: '#f5f5f5', padding: '10px', overflow: 'auto' }}
        >
          {JSON.stringify(formData, null, 2)}
        </pre>
      </details>
    </form>
  );
}

Key Learnings:

  1. Dynamic rendering với Object.keys().map()
  2. Generic handler scales to any number of fields
  3. Validation logic reusable
  4. Easy to add new fields - just update FIELDS object!

⭐⭐⭐ Exercise 3: Product Review Form with Validation (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Form với validation phức tạp
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là customer, tôi muốn review sản phẩm với rating và comment"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Fields: productName, rating (1-5), reviewTitle, reviewText, recommend (yes/no), email
 * - [ ] Rating: số nguyên từ 1-5, hiển thị stars ⭐
 * - [ ] Review title: 10-100 ký tự
 * - [ ] Review text: 50-500 ký tự
 * - [ ] Email: valid format
 * - [ ] Show errors onBlur (touched fields only)
 * - [ ] Character counters cho text fields
 * - [ ] Disable submit khi invalid
 *
 * 🎨 Technical Constraints:
 * - Object state cho form data
 * - Object state cho errors
 * - Object state cho touched
 * - Validators object với reusable validation functions
 *
 * 🚨 Edge Cases cần handle:
 * - Empty strings
 * - Spaces-only strings (use .trim())
 * - Rating out of range
 * - Email format invalid
 */

// ✅ NHIỆM VỤ CỦA BẠN:

function ProductReviewForm() {
  // TODO: State for form data
  const [formData, setFormData] = useState({
    productName: '',
    rating: '',
    reviewTitle: '',
    reviewText: '',
    recommend: '',
    email: '',
  });

  // TODO: State for errors
  const [errors, setErrors] = useState({});

  // TODO: State for touched fields
  const [touched, setTouched] = useState({});

  // TODO: Validation functions
  const validators = {
    productName: (value) => {
      // Required, min 2 chars
    },
    rating: (value) => {
      // Required, 1-5
    },
    reviewTitle: (value) => {
      // Required, 10-100 chars
    },
    reviewText: (value) => {
      // Required, 50-500 chars
    },
    recommend: (value) => {
      // Required
    },
    email: (value) => {
      // Required, valid email
    },
  };

  // TODO: Implement handlers
  const handleChange = (e) => {};
  const handleBlur = (e) => {};
  const handleSubmit = (e) => {};

  return (
    <form onSubmit={handleSubmit}>{/* TODO: Implement form fields */}</form>
  );
}

// 📝 Implementation Checklist:
// - [ ] All validators implemented
// - [ ] handleChange clears errors
// - [ ] handleBlur validates + marks touched
// - [ ] handleSubmit validates all
// - [ ] Star rating display (⭐⭐⭐⭐⭐)
// - [ ] Character counters
// - [ ] Error messages show only for touched fields
💡 Full Solution
jsx
function ProductReviewForm() {
  const [formData, setFormData] = useState({
    productName: '',
    rating: '',
    reviewTitle: '',
    reviewText: '',
    recommend: '',
    email: '',
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [submitted, setSubmitted] = useState(false);

  // ✅ Validation functions
  const validators = {
    productName: (value) => {
      if (!value.trim()) return 'Product name is required';
      if (value.trim().length < 2)
        return 'Product name must be at least 2 characters';
      return '';
    },

    rating: (value) => {
      if (!value) return 'Please select a rating';
      const num = Number(value);
      if (isNaN(num) || num < 1 || num > 5)
        return 'Rating must be between 1 and 5';
      return '';
    },

    reviewTitle: (value) => {
      if (!value.trim()) return 'Review title is required';
      const len = value.trim().length;
      if (len < 10) return `Title too short (${len}/10 minimum)`;
      if (len > 100) return `Title too long (${len}/100 maximum)`;
      return '';
    },

    reviewText: (value) => {
      if (!value.trim()) return 'Review text is required';
      const len = value.trim().length;
      if (len < 50) return `Review too short (${len}/50 minimum)`;
      if (len > 500) return `Review too long (${len}/500 maximum)`;
      return '';
    },

    recommend: (value) => {
      if (!value) return 'Please indicate if you would recommend this product';
      return '';
    },

    email: (value) => {
      if (!value.trim()) return 'Email is required';
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) return 'Invalid email format';
      return '';
    },
  };

  // ✅ Validate single field
  const validateField = (name, value) => {
    if (validators[name]) {
      return validators[name](value);
    }
    return '';
  };

  // ✅ Validate all fields
  const validateAll = () => {
    const newErrors = {};
    Object.keys(formData).forEach((field) => {
      const error = validateField(field, formData[field]);
      if (error) newErrors[field] = error;
    });
    return newErrors;
  };

  // ✅ Handle input change
  const handleChange = (e) => {
    const { name, value } = e.target;

    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));

    // Clear error when user types
    if (errors[name]) {
      setErrors((prev) => ({
        ...prev,
        [name]: '',
      }));
    }
  };

  // ✅ Handle blur
  const handleBlur = (e) => {
    const { name, value } = e.target;

    setTouched((prev) => ({
      ...prev,
      [name]: true,
    }));

    const error = validateField(name, value);
    if (error) {
      setErrors((prev) => ({
        ...prev,
        [name]: error,
      }));
    }
  };

  // ✅ Handle submit
  const handleSubmit = (e) => {
    e.preventDefault();

    // Mark all as touched
    const allTouched = Object.keys(formData).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {});
    setTouched(allTouched);

    // Validate all
    const validationErrors = validateAll();

    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    // Success!
    console.log('Review submitted:', formData);
    setSubmitted(true);

    // Reset form after 2 seconds
    setTimeout(() => {
      setFormData({
        productName: '',
        rating: '',
        reviewTitle: '',
        reviewText: '',
        recommend: '',
        email: '',
      });
      setErrors({});
      setTouched({});
      setSubmitted(false);
    }, 2000);
  };

  const isFormValid = Object.keys(validateAll()).length === 0;

  // Helper: Render stars
  const renderStars = (rating) => {
    return '⭐'.repeat(Number(rating) || 0);
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '700px', margin: '0 auto', padding: '20px' }}
    >
      <h2>📝 Product Review</h2>

      {submitted && (
        <div
          style={{
            padding: '15px',
            background: '#d4edda',
            color: '#155724',
            marginBottom: '20px',
            borderRadius: '4px',
          }}
        >
          ✅ Thank you for your review!
        </div>
      )}

      {/* Product Name */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Product Name *
        </label>
        <input
          name='productName'
          value={formData.productName}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder='e.g., iPhone 15 Pro'
          style={{
            width: '100%',
            padding: '10px',
            border:
              touched.productName && errors.productName
                ? '2px solid red'
                : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {touched.productName && errors.productName && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.productName}
          </p>
        )}
      </div>

      {/* Rating */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Rating * {formData.rating && renderStars(formData.rating)}
        </label>
        <select
          name='rating'
          value={formData.rating}
          onChange={handleChange}
          onBlur={handleBlur}
          style={{
            width: '100%',
            padding: '10px',
            border:
              touched.rating && errors.rating
                ? '2px solid red'
                : '1px solid #ccc',
            borderRadius: '4px',
          }}
        >
          <option value=''>Select rating...</option>
          <option value='5'>5 - Excellent</option>
          <option value='4'>4 - Good</option>
          <option value='3'>3 - Average</option>
          <option value='2'>2 - Poor</option>
          <option value='1'>1 - Terrible</option>
        </select>
        {touched.rating && errors.rating && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.rating}
          </p>
        )}
      </div>

      {/* Review Title */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Review Title *
          <span
            style={{ fontSize: '0.85em', fontWeight: 'normal', color: '#666' }}
          >
            {' '}
            ({formData.reviewTitle.trim().length}/10-100 chars)
          </span>
        </label>
        <input
          name='reviewTitle'
          value={formData.reviewTitle}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder='Sum up your review in one line'
          maxLength={100}
          style={{
            width: '100%',
            padding: '10px',
            border:
              touched.reviewTitle && errors.reviewTitle
                ? '2px solid red'
                : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {touched.reviewTitle && errors.reviewTitle && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.reviewTitle}
          </p>
        )}
      </div>

      {/* Review Text */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Your Review *
          <span
            style={{ fontSize: '0.85em', fontWeight: 'normal', color: '#666' }}
          >
            {' '}
            ({formData.reviewText.trim().length}/50-500 chars)
          </span>
        </label>
        <textarea
          name='reviewText'
          value={formData.reviewText}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder='Tell us what you think about this product...'
          rows={6}
          maxLength={500}
          style={{
            width: '100%',
            padding: '10px',
            border:
              touched.reviewText && errors.reviewText
                ? '2px solid red'
                : '1px solid #ccc',
            borderRadius: '4px',
            fontFamily: 'inherit',
          }}
        />
        {touched.reviewText && errors.reviewText && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.reviewText}
          </p>
        )}
      </div>

      {/* Recommend */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '10px', fontWeight: 'bold' }}
        >
          Would you recommend this product? *
        </label>
        <div>
          <label style={{ marginRight: '20px', cursor: 'pointer' }}>
            <input
              type='radio'
              name='recommend'
              value='yes'
              checked={formData.recommend === 'yes'}
              onChange={handleChange}
              onBlur={handleBlur}
            />{' '}
            Yes
          </label>
          <label style={{ cursor: 'pointer' }}>
            <input
              type='radio'
              name='recommend'
              value='no'
              checked={formData.recommend === 'no'}
              onChange={handleChange}
              onBlur={handleBlur}
            />{' '}
            No
          </label>
        </div>
        {touched.recommend && errors.recommend && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.recommend}
          </p>
        )}
      </div>

      {/* Email */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Email *{' '}
          <span
            style={{ fontSize: '0.85em', fontWeight: 'normal', color: '#666' }}
          >
            (for verification)
          </span>
        </label>
        <input
          type='email'
          name='email'
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder='you@example.com'
          style={{
            width: '100%',
            padding: '10px',
            border:
              touched.email && errors.email
                ? '2px solid red'
                : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {touched.email && errors.email && (
          <p style={{ color: 'red', fontSize: '0.9em', margin: '5px 0 0 0' }}>
            {errors.email}
          </p>
        )}
      </div>

      {/* Submit */}
      <button
        type='submit'
        disabled={!isFormValid}
        style={{
          width: '100%',
          padding: '15px',
          background: isFormValid ? '#007bff' : '#ccc',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          fontSize: '16px',
          fontWeight: 'bold',
          cursor: isFormValid ? 'pointer' : 'not-allowed',
        }}
      >
        Submit Review
      </button>

      {/* Debug */}
      <details style={{ marginTop: '30px' }}>
        <summary>🔍 Debug Info</summary>
        <div style={{ fontSize: '0.85em' }}>
          <p>
            <strong>Is Valid:</strong> {isFormValid ? 'Yes ✅' : 'No ❌'}
          </p>
          <p>
            <strong>Form Data:</strong>
          </p>
          <pre style={{ background: '#f5f5f5', padding: '10px' }}>
            {JSON.stringify(formData, null, 2)}
          </pre>
          <p>
            <strong>Errors:</strong>
          </p>
          <pre style={{ background: '#f5f5f5', padding: '10px' }}>
            {JSON.stringify(errors, null, 2)}
          </pre>
        </div>
      </details>
    </form>
  );
}

⭐⭐⭐⭐ Exercise 4: Multi-step Form (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Multi-step wizard form
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Requirements:
 * - 3 steps: Personal Info → Account Details → Preferences
 * - Step 1: firstName, lastName, email, phone
 * - Step 2: username, password, confirmPassword
 * - Step 3: newsletter (yes/no), notifications (yes/no), theme (light/dark)
 * - Next button disabled if current step invalid
 * - Back button (except step 1)
 * - Progress indicator (Step X of 3)
 * - Final review before submit
 *
 * State Design Questions:
 * 1. How to track current step?
 * 2. How to structure form data (1 object vs 3 objects)?
 * 3. How to validate per-step?
 * 4. How to prevent going to next step if invalid?
 *
 * ADR Template:
 * - Context: Multi-step form cần track progress + validate per step
 * - Decision: State structure của bạn
 * - Rationale: Tại sao?
 * - Consequences: Trade-offs
 *
 * 💻 PHASE 2: Implementation (30 phút)
 *
 * 🧪 PHASE 3: Testing (10 phút)
 * Manual test cases
 */

// ✅ NHIỆM VỤ CỦA BẠN:

function MultiStepForm() {
  // TODO: Design state
  // Consider:
  // - currentStep (number)
  // - formData (how to structure?)
  // - errors per step?

  // TODO: Implement step validation

  // TODO: Implement navigation (next/back)

  // TODO: Render current step conditionally

  return (
    <div>
      {/* Progress indicator */}
      {/* Step content */}
      {/* Navigation buttons */}
    </div>
  );
}
💡 Solution với ADR
jsx
/**
 * ADR: Multi-Step Form State Structure
 *
 * Decision:
 * - currentStep: number (1, 2, 3)
 * - formData: single flat object với tất cả fields
 * - errors: object với keys matching formData
 *
 * Rationale:
 * - Single formData object: dễ submit cuối cùng
 * - Flat structure: không cần nested validation
 * - Validators per field, check per step
 *
 * Consequences:
 * - Must define which fields belong to which step
 * - Validation logic coupled to step definition
 * - Trade-off: simplicity vs flexibility (OK for 3 steps)
 */

function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(1);

  const [formData, setFormData] = useState({
    // Step 1
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    // Step 2
    username: '',
    password: '',
    confirmPassword: '',
    // Step 3
    newsletter: false,
    notifications: false,
    theme: 'light',
  });

  const [errors, setErrors] = useState({});

  // Define steps
  const STEPS = {
    1: {
      title: 'Personal Information',
      fields: ['firstName', 'lastName', 'email', 'phone'],
    },
    2: {
      title: 'Account Details',
      fields: ['username', 'password', 'confirmPassword'],
    },
    3: {
      title: 'Preferences',
      fields: ['newsletter', 'notifications', 'theme'],
    },
  };

  // Validators
  const validators = {
    firstName: (v) => (!v.trim() ? 'First name required' : ''),
    lastName: (v) => (!v.trim() ? 'Last name required' : ''),
    email: (v) => {
      if (!v) return 'Email required';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return 'Invalid email';
      return '';
    },
    phone: (v) => {
      if (!v) return 'Phone required';
      if (!/^\d{10}$/.test(v.replace(/\D/g, '')))
        return 'Invalid phone (10 digits)';
      return '';
    },
    username: (v) => {
      if (!v) return 'Username required';
      if (v.length < 4) return 'Username min 4 chars';
      return '';
    },
    password: (v) => {
      if (!v) return 'Password required';
      if (v.length < 8) return 'Password min 8 chars';
      return '';
    },
    confirmPassword: (v) => {
      if (!v) return 'Please confirm password';
      if (v !== formData.password) return 'Passwords do not match';
      return '';
    },
  };

  // Validate current step
  const validateStep = (step) => {
    const stepFields = STEPS[step].fields;
    const stepErrors = {};

    stepFields.forEach((field) => {
      if (validators[field]) {
        const error = validators[field](formData[field]);
        if (error) stepErrors[field] = error;
      }
    });

    return stepErrors;
  };

  const isStepValid = () => {
    return Object.keys(validateStep(currentStep)).length === 0;
  };

  // Handlers
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;

    setFormData((prev) => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));

    // Clear error
    if (errors[name]) {
      setErrors((prev) => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  };

  const handleNext = () => {
    const stepErrors = validateStep(currentStep);

    if (Object.keys(stepErrors).length > 0) {
      setErrors(stepErrors);
      return;
    }

    if (currentStep < 3) {
      setCurrentStep((prev) => prev + 1);
    }
  };

  const handleBack = () => {
    if (currentStep > 1) {
      setCurrentStep((prev) => prev - 1);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // Validate all steps
    let allErrors = {};
    [1, 2, 3].forEach((step) => {
      allErrors = { ...allErrors, ...validateStep(step) };
    });

    if (Object.keys(allErrors).length > 0) {
      console.log('Form has errors:', allErrors);
      return;
    }

    console.log('Form submitted:', formData);
    alert('Registration successful!');

    // Reset
    setFormData({
      firstName: '',
      lastName: '',
      email: '',
      phone: '',
      username: '',
      password: '',
      confirmPassword: '',
      newsletter: false,
      notifications: false,
      theme: 'light',
    });
    setCurrentStep(1);
    setErrors({});
  };

  // Render step content
  const renderStep = () => {
    switch (currentStep) {
      case 1:
        return (
          <div>
            <h3>Personal Information</h3>
            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                First Name *
              </label>
              <input
                name='firstName'
                value={formData.firstName}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.firstName && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.firstName}
                </p>
              )}
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Last Name *
              </label>
              <input
                name='lastName'
                value={formData.lastName}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.lastName && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.lastName}
                </p>
              )}
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Email *
              </label>
              <input
                type='email'
                name='email'
                value={formData.email}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.email && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.email}
                </p>
              )}
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Phone *
              </label>
              <input
                type='tel'
                name='phone'
                value={formData.phone}
                onChange={handleChange}
                placeholder='1234567890'
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.phone && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.phone}
                </p>
              )}
            </div>
          </div>
        );

      case 2:
        return (
          <div>
            <h3>Account Details</h3>
            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Username *
              </label>
              <input
                name='username'
                value={formData.username}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.username && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.username}
                </p>
              )}
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Password *
              </label>
              <input
                type='password'
                name='password'
                value={formData.password}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.password && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.password}
                </p>
              )}
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Confirm Password *
              </label>
              <input
                type='password'
                name='confirmPassword'
                value={formData.confirmPassword}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              />
              {errors.confirmPassword && (
                <p style={{ color: 'red', fontSize: '0.9em' }}>
                  {errors.confirmPassword}
                </p>
              )}
            </div>
          </div>
        );

      case 3:
        return (
          <div>
            <h3>Preferences</h3>

            <div style={{ marginBottom: '15px' }}>
              <label
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  cursor: 'pointer',
                }}
              >
                <input
                  type='checkbox'
                  name='newsletter'
                  checked={formData.newsletter}
                  onChange={handleChange}
                  style={{ marginRight: '10px' }}
                />
                Subscribe to newsletter
              </label>
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  cursor: 'pointer',
                }}
              >
                <input
                  type='checkbox'
                  name='notifications'
                  checked={formData.notifications}
                  onChange={handleChange}
                  style={{ marginRight: '10px' }}
                />
                Enable notifications
              </label>
            </div>

            <div style={{ marginBottom: '15px' }}>
              <label style={{ display: 'block', marginBottom: '5px' }}>
                Theme
              </label>
              <select
                name='theme'
                value={formData.theme}
                onChange={handleChange}
                style={{ width: '100%', padding: '8px' }}
              >
                <option value='light'>Light</option>
                <option value='dark'>Dark</option>
              </select>
            </div>

            {/* Review */}
            <div
              style={{
                background: '#f5f5f5',
                padding: '15px',
                borderRadius: '4px',
                marginTop: '20px',
              }}
            >
              <h4>Review Your Information</h4>
              <p>
                <strong>Name:</strong> {formData.firstName} {formData.lastName}
              </p>
              <p>
                <strong>Email:</strong> {formData.email}
              </p>
              <p>
                <strong>Phone:</strong> {formData.phone}
              </p>
              <p>
                <strong>Username:</strong> {formData.username}
              </p>
              <p>
                <strong>Newsletter:</strong>{' '}
                {formData.newsletter ? 'Yes' : 'No'}
              </p>
              <p>
                <strong>Notifications:</strong>{' '}
                {formData.notifications ? 'Yes' : 'No'}
              </p>
              <p>
                <strong>Theme:</strong> {formData.theme}
              </p>
            </div>
          </div>
        );

      default:
        return null;
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}
    >
      <h2>Multi-Step Registration</h2>

      {/* Progress */}
      <div style={{ marginBottom: '30px' }}>
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            marginBottom: '10px',
          }}
        >
          {[1, 2, 3].map((step) => (
            <div
              key={step}
              style={{
                flex: 1,
                textAlign: 'center',
                padding: '10px',
                background: currentStep >= step ? '#007bff' : '#e0e0e0',
                color: currentStep >= step ? 'white' : '#666',
                margin: '0 5px',
                borderRadius: '4px',
                fontWeight: currentStep === step ? 'bold' : 'normal',
              }}
            >
              Step {step}
            </div>
          ))}
        </div>
        <p style={{ textAlign: 'center', color: '#666' }}>
          {STEPS[currentStep].title}
        </p>
      </div>

      {/* Step Content */}
      <div style={{ minHeight: '300px' }}>{renderStep()}</div>

      {/* Navigation */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          marginTop: '30px',
        }}
      >
        <button
          type='button'
          onClick={handleBack}
          disabled={currentStep === 1}
          style={{
            padding: '10px 20px',
            background: currentStep === 1 ? '#ccc' : '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: currentStep === 1 ? 'not-allowed' : 'pointer',
          }}
        >
          Back
        </button>

        {currentStep < 3 ? (
          <button
            type='button'
            onClick={handleNext}
            disabled={!isStepValid()}
            style={{
              padding: '10px 20px',
              background: isStepValid() ? '#007bff' : '#ccc',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: isStepValid() ? 'pointer' : 'not-allowed',
            }}
          >
            Next
          </button>
        ) : (
          <button
            type='submit'
            style={{
              padding: '10px 20px',
              background: '#28a745',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            Submit
          </button>
        )}

        {/**
         * ⚠️ LEARNING PITFALL- Có bẫy ở đoạn code render condition currentStep < 3 vừa rồi:
         *
         * 1. Hiện tượng: Đang ở Step 2, nhấn "Next", Form lập tức chạy handleSubmit (Step 3 hiện ra rồi biến mất ngay). 
         * “Tại sao chưa click Submit mà form vẫn submit?”
         * 2. Nguyên nhân (Race Condition): React reuse DOM node + event bubbling.
         * - Ở Step 2, nút là type="button". Khi click, setCurrentStep(3) được gọi.
         * - React render lại cực nhanh. Nó thấy ở vị trí đó vẫn là 1 thẻ <button>, nên nó giữ nguyên DOM node đó
         * và chỉ cập nhật thuộc tính type thành "submit" (để sang Step 3).
         * - Lúc này, sự kiện "click" từ ngón tay người dùng vẫn chưa kết thúc (vẫn đang trong chu kỳ event loop).
         * - Trình duyệt thấy một nút type="submit" vừa bị click -> Kích hoạt onSubmit của thẻ <form>.
         *
         * 👉 Hãy thử:
         * 1. Giữ nguyên code như hiện tại
         * 2. Click "Next" từ Step 2
         * 3. Quan sát: Step 3 xuất hiện rồi submit ngay
         *
         * Sau đó, thực hiện 2 cách fix:
         * Cách 1. Bỏ onSubmit khỏi <form>, đổi type="submit" ở step 3 thành type="button" và gán onClick cho nó 
         * -> Cách này React kiểm soát 100% flow, không dính browser default. 
         * Nhưng mất semantic HTML và người dùng không thể nhấn Enter để gửi form.
         * 
         * Cách 2. Giữ onSubmit, dùng key để tạo sự khác biệt cho DOM node, lúc này 2 nút là riêng biệt không tái sử dụng (reuse)
         * Giải quyết đúng gốc rễ việc tái sử dụng DOM của React.
         * Người dùng vẫn có thể nhấn Enter ở step cuối để submit (đúng chuẩn HTML). Code chuẩn ngữ nghĩa (semantic) của Form HTML hơn.
         * Hiểu thêm về công dụng của key trong Reactjs.
         * {currentStep < 3 ? (
              <button
                key="next-btn" // 👈 Thêm key 
                type='button'
                onClick={handleNext}
              >
                Next
              </button>
            ) : (
              <button
                key="submit-btn" // 👈 Key khác hoàn toàn
                type='submit'
              >
                Submit
              </button>
            )}
         */}
      </div>
    </form>
  );
}

⭐⭐⭐⭐⭐ Exercise 5: Dynamic Survey Form (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Production-ready dynamic survey form
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * - Survey với questions được define trong config
 * - Question types: text, textarea, select, radio, checkbox, number
 * - Conditional questions (show Q2 only if Q1 === 'yes')
 * - Required vs optional questions
 * - Validation per question type
 * - Progress bar (X of Y questions answered)
 * - Save draft to localStorage
 * - Export results as JSON
 *
 * 🏗️ Technical Design:
 * 1. Questions config (array of question objects)
 * 2. Answers state (object with question IDs as keys)
 * 3. Generic renderer for different question types
 * 4. Conditional logic evaluator
 * 5. Progress calculator (derived state)
 *
 * ✅ Production Checklist:
 * - [ ] Config-driven (easy to add questions)
 * - [ ] Generic question renderer
 * - [ ] Conditional logic works
 * - [ ] Validation per type
 * - [ ] Progress tracking
 * - [ ] localStorage persistence
 * - [ ] Export functionality
 * - [ ] Clear documentation
 */

// Sample survey config
const SURVEY_CONFIG = [
  {
    id: 'q1',
    type: 'radio',
    question: 'Have you used React before?',
    options: ['Yes', 'No'],
    required: true,
  },
  {
    id: 'q2',
    type: 'select',
    question: 'How long have you been using React?',
    options: ['< 6 months', '6-12 months', '1-2 years', '2+ years'],
    required: true,
    showIf: { questionId: 'q1', answer: 'Yes' }, // Conditional!
  },
  {
    id: 'q3',
    type: 'checkbox',
    question: 'Which features have you used? (select all)',
    options: ['Hooks', 'Context', 'Refs', 'Portals', 'Suspense'],
    required: false,
  },
  {
    id: 'q4',
    type: 'number',
    question: 'Rate React on scale 1-10',
    min: 1,
    max: 10,
    required: true,
  },
  {
    id: 'q5',
    type: 'textarea',
    question: 'What do you like most about React?',
    required: true,
    minLength: 20,
  },
];

// Implement the survey form!
// Hint: You'll need creative state management for checkboxes (array of selected values)
💡 Hint: State Structure
jsx
// Suggested state structure
const [answers, setAnswers] = useState({
  q1: '', // radio: string
  q2: '', // select: string
  q3: [], // checkbox: array
  q4: '', // number: string (convert when needed)
  q5: '', // textarea: string
});

// For conditional rendering, you'll need a function like:
const shouldShowQuestion = (question) => {
  if (!question.showIf) return true;
  const { questionId, answer } = question.showIf;
  return answers[questionId] === answer;
};
💡 Solution
jsx
import { useState, useEffect } from 'react';

// ────────────────────────────────────────────────
// CONFIG - Dễ dàng thêm/sửa/xoá câu hỏi
// ────────────────────────────────────────────────
const SURVEY_CONFIG = [
  {
    id: 'q1',
    type: 'radio',
    question: 'Bạn đã từng sử dụng React trước đây chưa?',
    options: ['Có', 'Chưa'],
    required: true,
  },
  {
    id: 'q2',
    type: 'select',
    question: 'Bạn đã sử dụng React được bao lâu?',
    options: ['Dưới 6 tháng', '6-12 tháng', '1-2 năm', 'Hơn 2 năm'],
    required: true,
    showIf: { questionId: 'q1', answer: 'Có' },
  },
  {
    id: 'q3',
    type: 'checkbox',
    question:
      'Bạn đã sử dụng những tính năng nào của React? (chọn tất cả phù hợp)',
    options: [
      'Hooks',
      'Context API',
      'useRef',
      'Portals',
      'Suspense',
      'Server Components',
    ],
    required: false,
  },
  {
    id: 'q4',
    type: 'number',
    question: 'Bạn đánh giá React bao nhiêu điểm trên thang 10?',
    min: 1,
    max: 10,
    required: true,
  },
  {
    id: 'q5',
    type: 'textarea',
    question: 'Điều bạn thích nhất ở React là gì?',
    required: true,
    minLength: 20,
    maxLength: 500,
  },
];

// ────────────────────────────────────────────────
// Helper: Kiểm tra câu hỏi có nên hiển thị không
// ────────────────────────────────────────────────
const shouldShowQuestion = (question, answers) => {
  if (!question.showIf) return true;

  const { questionId, answer } = question.showIf;
  return answers[questionId] === answer;
};

// ────────────────────────────────────────────────
// Helper: Validate một câu trả lời theo config
// ────────────────────────────────────────────────
const validateAnswer = (question, value) => {
  if (!question.required && !value) return '';

  switch (question.type) {
    case 'text':
    case 'textarea':
      if (question.required && (!value || value.trim() === '')) {
        return 'Câu hỏi này là bắt buộc';
      }
      if (question.minLength && value.trim().length < question.minLength) {
        return `Tối thiểu ${question.minLength} ký tự`;
      }
      if (question.maxLength && value.trim().length > question.maxLength) {
        return `Tối đa ${question.maxLength} ký tự`;
      }
      return '';

    case 'number':
      if (question.required && !value) return 'Vui lòng nhập số';
      const num = Number(value);
      if (isNaN(num)) return 'Vui lòng nhập số hợp lệ';
      if (question.min != null && num < question.min)
        return `Tối thiểu ${question.min}`;
      if (question.max != null && num > question.max)
        return `Tối đa ${question.max}`;
      return '';

    case 'radio':
    case 'select':
      if (question.required && !value) return 'Vui lòng chọn một lựa chọn';
      return '';

    case 'checkbox':
      if (question.required && (!Array.isArray(value) || value.length === 0)) {
        return 'Vui lòng chọn ít nhất một lựa chọn';
      }
      return '';

    default:
      return '';
  }
};

// ────────────────────────────────────────────────
// COMPONENT CHÍNH
// ────────────────────────────────────────────────
function DynamicSurveyForm() {
  // State chính lưu câu trả lời
  const [answers, setAnswers] = useState(() => {
    // Load draft từ localStorage nếu có
    const saved = localStorage.getItem('survey-draft');
    return saved ? JSON.parse(saved) : {};
  });

  const [touched, setTouched] = useState({});
  const [submitted, setSubmitted] = useState(false);

  // Lưu draft mỗi khi answers thay đổi (debounce nếu muốn tối ưu)
  useEffect(() => {
    localStorage.setItem('survey-draft', JSON.stringify(answers));
  }, [answers]);

  // ── Derived: Danh sách câu hỏi hiện tại (sau khi lọc conditional)
  const visibleQuestions = SURVEY_CONFIG.filter((q) =>
    shouldShowQuestion(q, answers),
  );

  // ── Derived: Tính progress (%)
  const answeredCount = visibleQuestions.filter((q) => {
    const val = answers[q.id];
    if (q.type === 'checkbox') return Array.isArray(val) && val.length > 0;
    return val !== undefined && val !== '' && val !== null;
  }).length;

  const progress = Math.round((answeredCount / visibleQuestions.length) * 100);

  // ── Validate toàn bộ form (dùng cho nút Submit)
  const getAllErrors = () => {
    const errors = {};
    visibleQuestions.forEach((q) => {
      const error = validateAnswer(q, answers[q.id]);
      if (error) errors[q.id] = error;
    });
    return errors;
  };

  const errors = getAllErrors();
  const isFormValid = Object.keys(errors).length === 0;

  // ── Handlers
  const handleChange = (questionId, value) => {
    setAnswers((prev) => ({
      ...prev,
      [questionId]: value,
    }));

    // Clear error khi đang gõ
    if (touched[questionId]) {
      setTouched((prev) => ({ ...prev, [questionId]: false }));
    }
  };

  const handleBlur = (questionId) => {
    setTouched((prev) => ({ ...prev, [questionId]: true }));
  };

  const handleCheckboxChange = (questionId, option, checked) => {
    setAnswers((prev) => {
      const current = prev[questionId] || [];
      if (checked) {
        return { ...prev, [questionId]: [...current, option] };
      } else {
        return {
          ...prev,
          [questionId]: current.filter((item) => item !== option),
        };
      }
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // Mark all visible questions as touched
    const allTouched = {};
    visibleQuestions.forEach((q) => {
      allTouched[q.id] = true;
    });
    setTouched(allTouched);

    if (!isFormValid) {
      alert('Vui lòng hoàn thành các câu hỏi bắt buộc!');
      return;
    }

    // ── Success
    console.log('Survey submitted:', answers);
    setSubmitted(true);

    // Clear draft sau khi submit thành công
    localStorage.removeItem('survey-draft');
    setAnswers({});
    setTouched({});
  };

  const handleExport = () => {
    const dataStr = JSON.stringify(answers, null, 2);
    const blob = new Blob([dataStr], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = 'survey-answers.json';
    link.click();
    URL.revokeObjectURL(url);
  };

  // ── Render
  if (submitted) {
    return (
      <div
        style={{
          textAlign: 'center',
          padding: '40px',
          maxWidth: 700,
          margin: '0 auto',
        }}
      >
        <h2 style={{ color: '#28a745' }}>
          Cảm ơn bạn đã hoàn thành khảo sát! 🎉
        </h2>
        <p>Bạn có thể xem lại kết quả trong console.</p>
        <button
          onClick={() => {
            setSubmitted(false);
            setAnswers({});
          }}
          style={{
            marginTop: 20,
            padding: '12px 24px',
            background: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: 6,
            fontSize: 16,
          }}
        >
          Làm lại khảo sát
        </button>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: 720, margin: '40px auto', padding: '0 20px' }}>
      <h1>Khảo sát về React</h1>

      {/* Progress bar */}
      <div style={{ margin: '20px 0' }}>
        <div
          style={{
            height: 12,
            background: '#e0e0e0',
            borderRadius: 6,
            overflow: 'hidden',
          }}
        >
          <div
            style={{
              width: `${progress}%`,
              height: '100%',
              background: progress === 100 ? '#28a745' : '#007bff',
              transition: 'width 0.4s ease',
            }}
          />
        </div>
        <p style={{ textAlign: 'center', marginTop: 8, color: '#555' }}>
          Đã hoàn thành {answeredCount}/{visibleQuestions.length} câu hỏi •{' '}
          {progress}%
        </p>
      </div>

      <form onSubmit={handleSubmit}>
        {visibleQuestions.map((question) => {
          const value = answers[question.id];
          const error = touched[question.id]
            ? validateAnswer(question, value)
            : '';
          const showError = !!error;

          return (
            <div
              key={question.id}
              style={{
                marginBottom: 32,
                padding: 20,
                background: '#f8f9fa',
                borderRadius: 8,
                border: showError ? '2px solid #dc3545' : '1px solid #dee2e6',
              }}
            >
              <label
                style={{
                  fontWeight: 'bold',
                  display: 'block',
                  marginBottom: 12,
                }}
              >
                {question.question}
                {question.required && (
                  <span style={{ color: '#dc3545' }}> *</span>
                )}
              </label>

              {/* ── Text / Textarea ── */}
              {(question.type === 'text' || question.type === 'textarea') && (
                <textarea
                  value={value || ''}
                  onChange={(e) => handleChange(question.id, e.target.value)}
                  onBlur={() => handleBlur(question.id)}
                  rows={question.type === 'textarea' ? 5 : 1}
                  style={{
                    width: '100%',
                    padding: 10,
                    border: showError
                      ? '2px solid #dc3545'
                      : '1px solid #ced4da',
                    borderRadius: 4,
                  }}
                />
              )}

              {/* ── Number ── */}
              {question.type === 'number' && (
                <input
                  type='number'
                  min={question.min}
                  max={question.max}
                  value={value ?? ''}
                  onChange={(e) => handleChange(question.id, e.target.value)}
                  onBlur={() => handleBlur(question.id)}
                  style={{
                    width: '100%',
                    padding: 10,
                    border: showError
                      ? '2px solid #dc3545'
                      : '1px solid #ced4da',
                    borderRadius: 4,
                  }}
                />
              )}

              {/* ── Select ── */}
              {question.type === 'select' && (
                <select
                  value={value || ''}
                  onChange={(e) => handleChange(question.id, e.target.value)}
                  onBlur={() => handleBlur(question.id)}
                  style={{
                    width: '100%',
                    padding: 10,
                    border: showError
                      ? '2px solid #dc3545'
                      : '1px solid #ced4da',
                    borderRadius: 4,
                  }}
                >
                  <option value=''>Chọn một lựa chọn...</option>
                  {question.options.map((opt) => (
                    <option
                      key={opt}
                      value={opt}
                    >
                      {opt}
                    </option>
                  ))}
                </select>
              )}

              {/* ── Radio ── */}
              {question.type === 'radio' && (
                <div
                  style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
                >
                  {question.options.map((opt) => (
                    <label
                      key={opt}
                      style={{ cursor: 'pointer' }}
                    >
                      <input
                        type='radio'
                        name={question.id}
                        value={opt}
                        checked={value === opt}
                        onChange={() => handleChange(question.id, opt)}
                        onBlur={() => handleBlur(question.id)}
                      />{' '}
                      {opt}
                    </label>
                  ))}
                </div>
              )}

              {/* ── Checkbox ── */}
              {question.type === 'checkbox' && (
                <div
                  style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
                >
                  {question.options.map((opt) => (
                    <label
                      key={opt}
                      style={{ cursor: 'pointer' }}
                    >
                      <input
                        type='checkbox'
                        checked={(value || []).includes(opt)}
                        onChange={(e) =>
                          handleCheckboxChange(
                            question.id,
                            opt,
                            e.target.checked,
                          )
                        }
                        onBlur={() => handleBlur(question.id)}
                      />{' '}
                      {opt}
                    </label>
                  ))}
                </div>
              )}

              {/* Error message */}
              {showError && (
                <p
                  style={{ color: '#dc3545', marginTop: 8, fontSize: '0.95em' }}
                >
                  {error}
                </p>
              )}
            </div>
          );
        })}

        <div style={{ marginTop: 40, textAlign: 'center' }}>
          <button
            type='submit'
            disabled={!isFormValid}
            style={{
              padding: '14px 40px',
              fontSize: 18,
              background: isFormValid ? '#28a745' : '#6c757d',
              color: 'white',
              border: 'none',
              borderRadius: 6,
              cursor: isFormValid ? 'pointer' : 'not-allowed',
              marginRight: 16,
            }}
          >
            Gửi khảo sát
          </button>

          <button
            type='button'
            onClick={handleExport}
            style={{
              padding: '14px 30px',
              fontSize: 16,
              background: '#6c757d',
              color: 'white',
              border: 'none',
              borderRadius: 6,
              cursor: 'pointer',
            }}
          >
            Export JSON
          </button>
        </div>
      </form>

      {/* Debug */}
      <details style={{ marginTop: 60 }}>
        <summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
          Debug: Current Answers
        </summary>
        <pre
          style={{
            background: '#f1f3f5',
            padding: 16,
            borderRadius: 6,
            overflow: 'auto',
            maxHeight: 400,
          }}
        >
          {JSON.stringify(answers, null, 2)}
        </pre>
      </details>
    </div>
  );
}

export default DynamicSurveyForm;
md
### Một số điểm nổi bật trong giải pháp:

- **Config-driven** → chỉ cần sửa `SURVEY_CONFIG` là thêm được câu hỏi mới
- **Conditional logic** hoạt động tốt với `showIf`
- **Checkbox** xử lý dưới dạng mảng
- **Validation** chi tiết theo từng loại input
- **Progress bar** dựa trên câu hỏi **hiển thị****đã trả lời**
- **localStorage draft** tự động lưu & khôi phục
- **Export JSON** tiện lợi
- **Touched pattern** → chỉ hiện lỗi khi người dùng tương tác
- UX thân thiện, dễ debug

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh Trade-offs

ApproachPros ✅Cons ❌When to Use 🎯
Controlled Components• React controls value
• Realtime validation
• Dynamic UI easy
• Single source of truth
• More boilerplate
• onChange cho mọi input
• Re-render on keystroke
• 95% of cases
• Forms cần validation
• Dynamic fields
Default choice
Uncontrolled Components
(useRef)
• Less code
• Giống HTML form
• Ít re-renders
• Không biết value trước submit
• Không validate realtime
• Hard to sync với UI
• File uploads
• Integration với non-React libs
• Rare edge cases
Multiple States
[a, setA], [b, setB]
• Simple cho 2-3 fields
• Clear separation
• Many setters
• Hard to group/reset
• Lots of onChange handlers
• Very simple forms (1-3 fields)
• Unrelated fields
Single Object State
{a, b, c}
• Scalable
• 1 generic handler
• Easy reset
• API-ready
• Need immutable updates
• Computed property syntax
Recommended for forms
• 4+ fields
• API submission
Validation: onChange• Instant feedback• Annoying (error while typing)• ❌ Not recommended alone
Validation: onBlur• Less annoying
• Show error when done
• Delayed feedback• ✅ Recommended primary
Validation: onSubmit• Final check
• Catch all errors
• Late feedback• ✅ Always include
Touched State• Only show errors for touched fields
• Better UX
• Extra state to manage• ✅ Production forms

Decision Tree

Q1: Có bao nhiêu field trong form?
├─ 1-2 field → State riêng lẻ OK
└─ 3+ field → Dùng state dạng object

Q2: Có cần validate realtime không?
├─ CÓ → Controlled components (luôn luôn)
└─ KHÔNG → Vẫn dùng controlled (uncontrolled chỉ nên dùng cho input file)

Q3: Khi nào hiển thị lỗi validate?
├─ onBlur → Chính (hiện sau khi người dùng rời field)
├─ onSubmit → Luôn luôn (kiểm tra cuối)
└─ onChange → Hạn chế (chỉ xoá lỗi, không tạo lỗi mới)

Q4: Tổ chức state object như thế nào?
├─ Object phẳng (đa số trường hợp)
└─ Object lồng nhau (khi có nhóm logic, ví dụ: địa chỉ thanh toán vs giao hàng)

Q5: Có cần track field đã chạm (touched) không?
├─ Form đơn giản → Không bắt buộc
└─ Form production → Có (UX tốt hơn)

Q6: Chiến lược timing validate?
└─ Pattern tốt nhất:
   • onBlur: Validate + đánh dấu touched
   • onChange: Xoá lỗi (không tạo lỗi mới)
   • onSubmit: Validate tất cả + đánh dấu tất cả touched

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Input Không Update ⭐

jsx
// 🐛 BUG: User gõ nhưng input không update
function BuggyForm() {
  const [email, setEmail] = useState('');

  return (
    <form>
      <input
        type='email'
        value={email}
        // BUG: Missing onChange!
      />
    </form>
  );
}

/**
 * 🔍 DEBUG QUESTIONS:
 * 1. Điều gì xảy ra khi user gõ?
 * 2. Tại sao input không update?
 * 3. Fix như thế nào?
 */
💡 Solution

Vấn đề:

  • Input có value={email} → controlled
  • Nhưng KHÔNG có onChange → React không update state
  • Input bị "frozen" ở giá trị initial

Tại sao:

User types → onChange event fires → React ignores (no handler)

                            State không update

                            value vẫn = ''

                            Input không đổi

Fix:

jsx
<input
  type='email'
  value={email}
  onChange={(e) => setEmail(e.target.value)} // ✅ Add handler
/>

Lesson: Controlled input = PHẢI có value + onChange


Bug 2: Generic Handler Không Work ⭐⭐

jsx
// 🐛 BUG: All inputs update cùng lúc
function BuggyMultiForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
  });

  const handleChange = (e) => {
    setFormData({
      [e.target.name]: e.target.value, // BUG: Không spread prev
    });
  };

  return (
    <form>
      <input
        name='firstName'
        value={formData.firstName}
        onChange={handleChange}
      />
      <input
        name='lastName'
        value={formData.lastName}
        onChange={handleChange}
      />
    </form>
  );
}

/**
 * 🔍 DEBUG QUESTIONS:
 * 1. Gõ "John" vào firstName. formData sẽ là gì?
 * 2. Sau đó gõ "Doe" vào lastName. formData sẽ là gì?
 * 3. firstName có còn "John" không? Tại sao?
 */
💡 Solution

Vấn đề:

jsx
// ❌ BAD: Overwrite entire object
setFormData({
  [e.target.name]: e.target.value,
});

// After typing "John" in firstName:
formData = { firstName: 'John' }; // ❌ lastName bị mất!

// After typing "Doe" in lastName:
formData = { lastName: 'Doe' }; // ❌ firstName bị mất!

Fix:

jsx
// ✅ GOOD: Spread prev, then override
const handleChange = (e) => {
  setFormData((prev) => ({
    ...prev, // Keep old values
    [e.target.name]: e.target.value,
  }));
};

Lesson: Immutable updates! Spread ...prev trước khi override.


Bug 3: Passwords Don't Match Validation ⭐⭐⭐

jsx
// 🐛 BUG: "Passwords don't match" không update khi password changes
function BuggyPasswordForm() {
  const [formData, setFormData] = useState({
    password: '',
    confirmPassword: '',
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;

    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;

    if (name === 'confirmPassword') {
      if (value !== formData.password) {
        setErrors((prev) => ({
          ...prev,
          confirmPassword: 'Passwords do not match',
        }));
      }
    }
  };

  return (
    <form>
      <input
        type='password'
        name='password'
        value={formData.password}
        onChange={handleChange}
      />

      <input
        type='password'
        name='confirmPassword'
        value={formData.confirmPassword}
        onChange={handleChange}
        onBlur={handleBlur}
      />

      {errors.confirmPassword && (
        <p style={{ color: 'red' }}>{errors.confirmPassword}</p>
      )}
    </form>
  );
}

/**
 * 🔍 DEBUG SCENARIO:
 * 1. User gõ "password123" vào password field
 * 2. User gõ "password123" vào confirmPassword → onBlur → No error ✅
 * 3. User quay lại password field, đổi thành "newpass456"
 * 4. 🐛 ERROR vẫn không hiện! Tại sao?
 * 5. Làm thế nào để fix?
 */
💡 Solution

Vấn đề:

  • Khi user đổi password, error KHÔNG được re-validate
  • confirmPassword onBlur chỉ chạy khi user blur confirmPassword field
  • Nếu user đổi password sau, confirmPassword không được check lại

Fix 1: Re-validate confirmPassword khi password changes

jsx
const handleChange = (e) => {
  const { name, value } = e.target;

  setFormData((prev) => ({
    ...prev,
    [name]: value,
  }));

  // ✅ If changing password, re-check confirmPassword
  if (name === 'password' && formData.confirmPassword) {
    if (value !== formData.confirmPassword) {
      setErrors((prev) => ({
        ...prev,
        confirmPassword: 'Passwords do not match',
      }));
    } else {
      // Clear error if now they match
      setErrors((prev) => {
        const newErrors = { ...prev };
        delete newErrors.confirmPassword;
        return newErrors;
      });
    }
  }
};

Fix 2: Clear error onChange (better UX)

jsx
const handleChange = (e) => {
  const { name, value } = e.target;

  setFormData((prev) => ({
    ...prev,
    [name]: value,
  }));

  // ✅ Clear error when user types (good UX)
  if (errors[name]) {
    setErrors((prev) => {
      const newErrors = { ...prev };
      delete newErrors[name];
      return newErrors;
    });
  }

  // ✅ Cross-field validation
  if (name === 'password' && formData.confirmPassword) {
    validateConfirmPassword(value, formData.confirmPassword);
  }
};

Lesson: Cross-field validation cần update khi ANY related field changes!


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

Knowledge Check

  • [ ] Tôi hiểu controlled vs uncontrolled components
  • [ ] Tôi biết khi nào dùng object state vs multiple states
  • [ ] Tôi có thể implement generic onChange handler với e.target.name
  • [ ] Tôi hiểu validation timing (onChange vs onBlur vs onSubmit)
  • [ ] Tôi biết cách implement "touched" state pattern
  • [ ] Tôi có thể validate cross-field dependencies (password match)
  • [ ] Tôi hiểu tại sao cần e.preventDefault() trong onSubmit
  • [ ] Tôi có thể structure validators thành reusable functions
  • [ ] Tôi biết cách clear form sau submit
  • [ ] Tôi hiểu trade-offs của mỗi validation approach

Code Review Checklist

Khi review code form:

Cấu trúc State:

  • [ ] Các field liên quan được nhóm trong object (không quá nhiều state tách rời)
  • [ ] Update bất biến (spread ...prev)
  • [ ] Không lưu derived state (tính isValid, không store)

Controlled Components:

  • [ ] Mọi input đều có prop value
  • [ ] Mọi input đều có handler onChange
  • [ ] Thuộc tính name của input khớp với key trong state

Validation:

  • [ ] Chỉ hiển thị lỗi cho field đã touched (UX tốt)
  • [ ] onBlur validate field
  • [ ] onChange xoá lỗi (không hiển thị lỗi khi đang gõ)
  • [ ] onSubmit validate tất cả
  • [ ] Có xử lý validate giữa các field

Submit Handler:

  • [ ] Có e.preventDefault()
  • [ ] Validate lần cuối trước khi submit
  • [ ] Xử lý khi thành công (clear form, hiển thị message)
  • [ ] Xử lý khi có lỗi

UX:

  • [ ] Disable nút submit khi form không hợp lệ
  • [ ] Thông báo lỗi rõ ràng, dễ hiểu
  • [ ] Bộ đếm ký tự khi cần
  • [ ] Loading state (sẽ học với useEffect)

🏠 BÀI TẬP VỀ NHÀ

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

Exercise: Fix Broken Forms

Cho 3 broken forms. Tìm và fix bugs:

  1. Form 1: Input không update khi gõ
💡 Xem code bị lỗi + giải thích + code đã sửa

Code bị lỗi:

jsx
function Form1() {
  const [email, setEmail] = useState('');
  return (
    <form>
      <input
        type='email'
        value={email}
      />
      <p>Bạn gõ: {email}</p>
    </form>
  );
}

Nguyên nhân:
Controlled input nhưng thiếu onChange → React không cập nhật state → input bị "đóng băng".

Code đã sửa:

jsx
function Form1Fixed() {
  const [email, setEmail] = useState('');

  return (
    <form>
      <input
        type='email'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder='example@gmail.com'
      />
      <p>Bạn đang gõ: {email || '(chưa nhập)'}</p>
    </form>
  );
}

Bài học: Controlled input phải có cả value + onChange.


  1. Form 2: Submit không work (page reload)
💡 Xem code bị lỗi + giải thích + code đã sửa

Code bị lỗi:

jsx
function Form2() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = () => {
    console.log({ username, password });
    alert('Đăng nhập!');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type='password'
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type='submit'>Đăng nhập</button>
    </form>
  );
}

Nguyên nhân:
Không gọi e.preventDefault() → browser thực hiện submit mặc định → reload trang.

Code đã sửa:

jsx
function Form2Fixed() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault(); // ← Fix quan trọng nhất
    console.log('Đăng nhập:', { username, password });
    setUsername('');
    setPassword('');
    alert('Đăng nhập thành công (không reload)!');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type='password'
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type='submit'>Đăng nhập</button>
    </form>
  );
}

Bài học: Trong React, luôn gọi e.preventDefault() trong hàm onSubmit.


  1. Form 3: Email validation sai (accept invalid emails)
💡 Xem code bị lỗi + giải thích + code đã sửa

Code bị lỗi:

jsx
function Form3() {
  const [email, setEmail] = useState('');

  const isValid = email.includes('@');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!isValid) {
      alert('Email sai!');
      return;
    }
    alert('Email ok: ' + email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='email'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type='submit'>Gửi</button>
    </form>
  );
}

Nguyên nhân:
Chỉ kiểm tra có @ → chấp nhận các email sai như:
abc@
@gmail.com
a@b
hello@world

Code đã sửa (phiên bản tốt hơn):

jsx
function Form3Fixed() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const validate = (value) => {
    if (!value.trim()) return 'Vui lòng nhập email';

    const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return regex.test(value) ? '' : 'Email không đúng định dạng';
  };

  const handleChange = (e) => {
    const val = e.target.value;
    setEmail(val);
    setError(validate(val));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const err = validate(email);
    if (err) {
      setError(err);
      alert('Vui lòng sửa email!');
      return;
    }
    alert('Email hợp lệ: ' + email);
    setEmail('');
    setError('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='email'
        value={email}
        onChange={handleChange}
        onBlur={() => setError(validate(email))}
      />
      {error && <p style={{ color: 'red', fontSize: '0.9em' }}>{error}</p>}
      <button type='submit'>Gửi</button>
    </form>
  );
}

Bài học:
Validation email cần regex tốt hơn + nên kiểm tra cả trường hợp rỗng.

Tóm tắt 3 lỗi phổ biến cần nhớ:

  1. Thiếu onChange → input không gõ được
  2. Quên e.preventDefault() → trang reload khi submit
  3. Validation quá yếu → cho qua email sai

Nâng cao (60 phút)

Exercise: Job Application Form

Tạo form apply job với:

  • Personal: name, email, phone
  • Experience: yearsOfExperience (number), currentRole, resume (text describing experience)
  • Motivation: why (textarea min 100 chars)
  • Validation realtime + onSubmit
  • Touched state pattern
  • Character counters
  • Progress indicator (X/Y fields completed)

Requirements:

  • ✅ Object state
  • ✅ Generic handlers
  • ✅ Proper validation timing
  • ✅ Production-ready error handling
💡 Xem đáp án
jsx
import { useState } from 'react';

function JobApplicationForm() {
  // ─── State chính ───────────────────────────────────────────────
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: '',
    yearsOfExperience: '',
    currentRole: '',
    resume: '', // mô tả kinh nghiệm (text)
    why: '', // lý do ứng tuyển (textarea)
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // ─── Validation rules ─────────────────────────────────────────
  const validateField = (name, value) => {
    switch (name) {
      case 'name':
        if (!value.trim()) return 'Họ và tên là bắt buộc';
        if (value.trim().length < 2) return 'Tên quá ngắn';
        return '';

      case 'email':
        if (!value.trim()) return 'Email là bắt buộc';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
          return 'Email không đúng định dạng';
        return '';

      case 'phone':
        if (!value.trim()) return 'Số điện thoại là bắt buộc';
        if (!/^(0|\+84)[0-9]{9,10}$/.test(value.replace(/\s/g, '')))
          return 'Số điện thoại không hợp lệ (10-11 số, bắt đầu bằng 0 hoặc +84)';
        return '';

      case 'yearsOfExperience':
        if (value === '') return 'Vui lòng nhập số năm kinh nghiệm';
        const num = Number(value);
        if (isNaN(num) || num < 0) return 'Vui lòng nhập số ≥ 0';
        return '';

      case 'currentRole':
        if (!value.trim()) return 'Vị trí hiện tại là bắt buộc';
        return '';

      case 'resume':
        if (!value.trim()) return 'Mô tả kinh nghiệm là bắt buộc';
        if (value.trim().length < 50)
          return `Mô tả quá ngắn (${value.trim().length}/50 ký tự tối thiểu)`;
        return '';

      case 'why':
        if (!value.trim()) return 'Lý do ứng tuyển là bắt buộc';
        if (value.trim().length < 100)
          return `Nội dung quá ngắn (${value.trim().length}/100 ký tự tối thiểu)`;
        return '';

      default:
        return '';
    }
  };

  // ─── Generic change handler ───────────────────────────────────
  const handleChange = (e) => {
    const { name, value } = e.target;

    setFormData((prev) => ({ ...prev, [name]: value }));

    // Xóa lỗi ngay khi người dùng gõ (UX tốt)
    if (errors[name]) {
      setErrors((prev) => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  };

  // ─── Blur → validate field ────────────────────────────────────
  const handleBlur = (e) => {
    const { name, value } = e.target;
    setTouched((prev) => ({ ...prev, [name]: true }));

    const error = validateField(name, value);
    if (error) {
      setErrors((prev) => ({ ...prev, [name]: error }));
    } else {
      setErrors((prev) => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  };

  // ─── Validation toàn bộ form (dùng cho nút submit & progress) ──
  const getAllErrors = () => {
    const allErrors = {};
    Object.keys(formData).forEach((field) => {
      const err = validateField(field, formData[field]);
      if (err) allErrors[field] = err;
    });
    return allErrors;
  };

  const allErrors = getAllErrors();
  const isFormValid = Object.keys(allErrors).length === 0;

  // ─── Progress: đếm field đã hoàn thành hợp lệ ─────────────────
  const requiredFields = [
    'name',
    'email',
    'phone',
    'yearsOfExperience',
    'currentRole',
    'resume',
    'why',
  ];

  const completedCount = requiredFields.filter((field) => {
    const val = formData[field];
    if (field === 'why') return val.trim().length >= 100;
    if (field === 'resume') return val.trim().length >= 50;
    if (field === 'yearsOfExperience')
      return val !== '' && !isNaN(Number(val)) && Number(val) >= 0;
    return val.trim() !== '';
  }).length;

  const progress = Math.round((completedCount / requiredFields.length) * 100);

  // ─── Submit ───────────────────────────────────────────────────
  const handleSubmit = (e) => {
    e.preventDefault();

    // Mark tất cả field là touched để hiện hết lỗi
    const allTouched = {};
    requiredFields.forEach((f) => (allTouched[f] = true));
    setTouched(allTouched);

    const validationErrors = getAllErrors();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      alert('Vui lòng kiểm tra lại các trường có lỗi.');
      return;
    }

    // ── Thành công ──────────────────────────────────────────────
    console.log('Đơn ứng tuyển:', formData);
    alert(
      'Đơn ứng tuyển đã được gửi thành công!\n\n(Có thể xem console để kiểm tra dữ liệu)',
    );

    // Reset form
    setFormData({
      name: '',
      email: '',
      phone: '',
      yearsOfExperience: '',
      currentRole: '',
      resume: '',
      why: '',
    });
    setErrors({});
    setTouched({});
  };

  // ─── Render ───────────────────────────────────────────────────
  return (
    <div style={{ maxWidth: 680, margin: '40px auto', padding: '0 20px' }}>
      <h1>Ứng tuyển vị trí Front-end Developer</h1>

      {/* Progress bar */}
      <div style={{ margin: '24px 0' }}>
        <div
          style={{
            height: 10,
            background: '#e9ecef',
            borderRadius: 5,
            overflow: 'hidden',
          }}
        >
          <div
            style={{
              width: `${progress}%`,
              height: '100%',
              background: progress === 100 ? '#28a745' : '#0d6efd',
              transition: 'width 0.4s ease',
            }}
          />
        </div>
        <p style={{ textAlign: 'center', marginTop: 8, color: '#495057' }}>
          Đã hoàn thành {completedCount}/{requiredFields.length} trường •{' '}
          {progress}%
        </p>
      </div>

      <form
        onSubmit={handleSubmit}
        noValidate
      >
        <fieldset style={{ marginBottom: 32 }}>
          <legend>Thông tin cá nhân</legend>

          <div style={{ marginBottom: 20 }}>
            <label>Họ và tên *</label>
            <input
              name='name'
              value={formData.name}
              onChange={handleChange}
              onBlur={handleBlur}
              style={{
                borderColor:
                  touched.name && errors.name ? '#dc3545' : '#ced4da',
              }}
            />
            {touched.name && errors.name && (
              <div className='error'>{errors.name}</div>
            )}
          </div>

          <div style={{ marginBottom: 20 }}>
            <label>Email *</label>
            <input
              type='email'
              name='email'
              value={formData.email}
              onChange={handleChange}
              onBlur={handleBlur}
              style={{
                borderColor:
                  touched.email && errors.email ? '#dc3545' : '#ced4da',
              }}
            />
            {touched.email && errors.email && (
              <div className='error'>{errors.email}</div>
            )}
          </div>

          <div style={{ marginBottom: 20 }}>
            <label>Số điện thoại *</label>
            <input
              type='tel'
              name='phone'
              value={formData.phone}
              onChange={handleChange}
              onBlur={handleBlur}
              style={{
                borderColor:
                  touched.phone && errors.phone ? '#dc3545' : '#ced4da',
              }}
            />
            {touched.phone && errors.phone && (
              <div className='error'>{errors.phone}</div>
            )}
          </div>
        </fieldset>

        <fieldset style={{ marginBottom: 32 }}>
          <legend>Kinh nghiệm làm việc</legend>

          <div style={{ marginBottom: 20 }}>
            <label>Số năm kinh nghiệm *</label>
            <input
              type='number'
              min='0'
              name='yearsOfExperience'
              value={formData.yearsOfExperience}
              onChange={handleChange}
              onBlur={handleBlur}
              style={{
                borderColor:
                  touched.yearsOfExperience && errors.yearsOfExperience
                    ? '#dc3545'
                    : '#ced4da',
              }}
            />
            {touched.yearsOfExperience && errors.yearsOfExperience && (
              <div className='error'>{errors.yearsOfExperience}</div>
            )}
          </div>

          <div style={{ marginBottom: 20 }}>
            <label>Vị trí / vai trò hiện tại *</label>
            <input
              name='currentRole'
              value={formData.currentRole}
              onChange={handleChange}
              onBlur={handleBlur}
              placeholder='VD: Senior React Developer'
              style={{
                borderColor:
                  touched.currentRole && errors.currentRole
                    ? '#dc3545'
                    : '#ced4da',
              }}
            />
            {touched.currentRole && errors.currentRole && (
              <div className='error'>{errors.currentRole}</div>
            )}
          </div>

          <div style={{ marginBottom: 20 }}>
            <label>Mô tả kinh nghiệm làm việc *</label>
            <textarea
              name='resume'
              value={formData.resume}
              onChange={handleChange}
              onBlur={handleBlur}
              rows={5}
              placeholder='Mô tả ngắn gọn về dự án, công nghệ đã sử dụng, thành tựu nổi bật...'
              style={{
                borderColor:
                  touched.resume && errors.resume ? '#dc3545' : '#ced4da',
              }}
            />
            <div className='counter'>
              {formData.resume.trim().length} / tối thiểu 50 ký tự
            </div>
            {touched.resume && errors.resume && (
              <div className='error'>{errors.resume}</div>
            )}
          </div>
        </fieldset>

        <fieldset style={{ marginBottom: 32 }}>
          <legend>Lý do ứng tuyển</legend>

          <div style={{ marginBottom: 20 }}>
            <label>Tại sao bạn muốn làm việc tại công ty chúng tôi? *</label>
            <textarea
              name='why'
              value={formData.why}
              onChange={handleChange}
              onBlur={handleBlur}
              rows={6}
              placeholder='Hãy chia sẻ động lực, điều bạn mong muốn học hỏi, đóng góp...'
              style={{
                borderColor: touched.why && errors.why ? '#dc3545' : '#ced4da',
              }}
            />
            <div className='counter'>
              {formData.why.trim().length} / tối thiểu 100 ký tự
            </div>
            {touched.why && errors.why && (
              <div className='error'>{errors.why}</div>
            )}
          </div>
        </fieldset>

        <button
          type='submit'
          disabled={!isFormValid}
          className={`submit-btn ${isFormValid ? 'valid' : 'invalid'}`}
        >
          Gửi đơn ứng tuyển
        </button>

        {!isFormValid && (
          <p style={{ color: '#dc3545', marginTop: 12, textAlign: 'center' }}>
            Vui lòng hoàn thành tất cả các trường bắt buộc và sửa lỗi (nếu có).
          </p>
        )}
      </form>

      {/* ── CSS inline cho gọn ────────────────────────────────────── */}
      <style jsx>{`
        label {
          display: block;
          margin-bottom: 6px;
          font-weight: 600;
        }
        input,
        textarea {
          width: 100%;
          padding: 10px;
          border: 1px solid #ced4da;
          border-radius: 6px;
          font-size: 16px;
          box-sizing: border-box;
        }
        input:focus,
        textarea:focus {
          outline: none;
          border-color: #0d6efd;
          box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
        }
        .error {
          color: #dc3545;
          font-size: 0.9em;
          margin-top: 6px;
        }
        .counter {
          font-size: 0.85em;
          color: #6c757d;
          margin-top: 4px;
          text-align: right;
        }
        .submit-btn {
          width: 100%;
          padding: 14px;
          font-size: 17px;
          font-weight: 600;
          color: white;
          border: none;
          border-radius: 8px;
          cursor: pointer;
          transition: all 0.2s;
        }
        .submit-btn.valid {
          background: #28a745;
        }
        .submit-btn.invalid {
          background: #6c757d;
          cursor: not-allowed;
        }
        fieldset {
          border: 1px solid #dee2e6;
          border-radius: 8px;
          padding: 20px;
          margin-bottom: 32px;
        }
        legend {
          font-weight: bold;
          padding: 0 12px;
          color: #0d6efd;
        }
      `}</style>
    </div>
  );
}

export default JobApplicationForm;

Các điểm chính đã đáp ứng:

  • Object state duy nhất cho toàn bộ form
  • Generic handler handleChange + handleBlur
  • Validation timing hợp lý:
    • onChange → xóa lỗi (UX mượt)
    • onBlur → validate & hiện lỗi
    • onSubmit → validate toàn bộ + mark all touched
  • Touched pattern → chỉ hiện lỗi sau khi người dùng tương tác
  • Character counters cho resumewhy
  • Progress indicator dựa trên số field hoàn thành hợp lệ
  • Error handling production-ready (inline style + class cho nút submit)

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Forms

  2. React Docs - Managing State

Đọc thêm

  1. Form Validation Best Practices

  2. HTML Form Validation


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

Kiến thức nền (đã học)

  • Ngày 12: useState patterns - Immutability, functional updates, object state
  • Ngày 11: useState basics
  • Ngày 5: Event handling - e.target, e.preventDefault()

Hướng tới (sẽ học)

  • Ngày 14: Lifting State Up - Share form state giữa components
  • Ngày 17-21: useEffect - Persist forms to localStorage, debounce validation
  • Ngày 36-38: Forms Deep Dive - Formik, React Hook Form, advanced patterns

💡 SENIOR INSIGHTS

Cân Nhắc Production

Performance:

jsx
// ⚠️ Re-render on every keystroke
function ExpensiveForm() {
  const [formData, setFormData] = useState({...});

  // ❌ Expensive computation runs every render
  const validationResult = expensiveValidation(formData);

  // ✅ Will learn useMemo in Ngày 23 to optimize
  // For now: Keep validators fast & simple
}

Accessibility:

jsx
// ✅ Production-ready accessible form
<form>
  <label htmlFor='email'>Email</label>
  <input
    id='email'
    name='email'
    type='email'
    value={formData.email}
    onChange={handleChange}
    aria-invalid={!!errors.email}
    aria-describedby={errors.email ? 'email-error' : undefined}
  />
  {errors.email && (
    <p
      id='email-error'
      role='alert'
    >
      {errors.email}
    </p>
  )}
</form>

Security:

jsx
// ⚠️ Never trust client-side validation alone!
const handleSubmit = async (e) => {
  e.preventDefault();

  // ✅ Client validation (UX)
  if (!isValid) return;

  try {
    // ✅ Server MUST validate again (security)
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(formData),
    });

    // Handle server validation errors
    if (!response.ok) {
      const serverErrors = await response.json();
      setErrors(serverErrors);
    }
  } catch (err) {
    // Handle network errors
  }
};

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

Junior:

Q: "Controlled component là gì?"

A: Component mà React controls input value through state. Input có value={state}onChange={setState}. React là single source of truth.

Mid:

Q: "Làm thế nào handle form với nhiều inputs efficiently?"

A: Dùng object state + generic handler với e.target.name. Mỗi input có name attribute match state key. Handler dùng computed property [name]: value với spread operator.

Senior:

Q: "Thiết kế chiến lược validate cho form phức tạp."

A: Cách tiếp cận nhiều lớp:

  1. onBlur: Validate field, đánh dấu touched (hiển thị lỗi)
  2. onChange: Xoá lỗi (UX tốt khi đang gõ)
  3. onSubmit: Validate tất cả, đánh dấu tất cả touched
  4. Cross-field: Validate lại các field phụ thuộc khi field liên quan thay đổi
  5. Server: Luôn validate phía server (bảo mật)
  6. Cân nhắc debounce cho validate async (email, username)

War Stories

Story 1: The Missing e.preventDefault() Bug

Junior dev tạo form, mọi thứ work local nhưng production bị bug lạ: form submit xong page bị blank. Root cause: Quên e.preventDefault() → browser submit form → page reload → blank page.

Lesson: LUÔN preventDefault trong onSubmit React forms!

Story 2: The Stale Password Validation

Người dùng than phiền: “Form báo mật khẩu không khớp dù tôi nhập y hệt nhau!” Debug mãi mới phát hiện nguyên nhân: ô password có bật autocomplete, nên khi mật khẩu thay đổi, validation của ô confirm password không được chạy lại.

Cách fix: mỗi khi password thay đổi thì re-validate confirm password. Bài học rút ra: Validation giữa nhiều field (cross-field validation) phải xử lý tất cả các thay đổi liên quan, không chỉ mỗi field đang nhập.

Story 3: The Performance Killer Form

Form có 50 field nên mỗi lần gõ phím đều bị lag ~100ms. Profiling mới thấy nguyên nhân: mỗi onChange lại chạy regex validation cho toàn bộ 50 field.

Cách fix:

– Khi gõ (onChange) chỉ validate field đang thay đổi

– Dời việc validate toàn bộ sang lúc onBlur

Bài học: Thời điểm chạy validation ảnh hưởng rất lớn đến performance.

🎯 PREVIEW NGÀY MAI

Ngày 14: Lifting State Up

Hôm nay đã master forms với local state. Ngày mai sẽ học:

  • Share state giữa sibling components
  • Lift state lên parent
  • Inverse data flow (child → parent communication)
  • When to lift vs when to keep local
  • Props drilling và cách giảm thiểu

Preview challenge: Tạo shopping cart nơi ProductList và CartSummary cần share state!

Hôm nay: Forms mastery ✅
Ngày mai: State communication 🎯


🎊 CHÚC MỪNG! Bạn đã hoàn thành Ngày 13!

Hôm nay bạn đã master:

  1. ✅ Controlled vs Uncontrolled components
  2. ✅ Multiple inputs với object state
  3. ✅ Validation strategies (onBlur, onChange, onSubmit)
  4. ✅ Touched state pattern
  5. ✅ Cross-field validation
  6. ✅ Production-ready form patterns

Forms là nền tảng của hầu hết web apps. Master forms = master React!

Pro Tip: Mọi form pattern học hôm nay sẽ scale đến bất kỳ form nào - từ login đơn giản đến checkout phức tạp với 100+ fields!

💪 Keep practicing! Tomorrow: State communication!

Personal tech knowledge base