Skip to content

📅 NGÀY 41: React Hook Form - Basics

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

  • [ ] Hiểu vấn đề của form trong React và tại sao cần library
  • [ ] Nắm vững React Hook Form API cơ bản (useForm, register, handleSubmit)
  • [ ] Biết cách validation với built-in rules
  • [ ] Hiểu performance benefits của uncontrolled components
  • [ ] So sánh được controlled vs uncontrolled forms

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

  1. Controlled Components (Ngày 13): Form inputs với useState hoạt động như thế nào?
  2. Custom Hooks (Ngày 24): Làm sao extract logic vào custom hook?
  3. Performance (Ngày 32-34): Re-render xảy ra khi nào? useMemo/useCallback dùng để làm gì?

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

1.1 Vấn Đề Thực Tế - Forms trong React

Tình huống: Bạn cần build form đăng ký với 10 fields

Approach 1: Manual với useState (Traditional)

jsx
function RegistrationForm() {
  // 😱 10 useState cho 10 fields!
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [phone, setPhone] = useState('');
  const [address, setAddress] = useState('');
  const [city, setCity] = useState('');
  const [country, setCountry] = useState('');
  const [zipCode, setZipCode] = useState('');

  // 😱 10 error states!
  const [emailError, setEmailError] = useState('');
  const [passwordError, setPasswordError] = useState('');
  // ... 8 more

  // 😱 10 touched states!
  const [emailTouched, setEmailTouched] = useState(false);
  // ... 9 more

  // 😱 10 onChange handlers!
  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    validateEmail(e.target.value);
  };
  // ... 9 more

  // 😱 Performance nightmare: 30 re-renders per keystroke!
}

Vấn đề:

  • 🔥 Quá nhiều boilerplate code
  • 🔥 Re-render mỗi lần gõ 1 ký tự (toàn bộ component)
  • 🔥 Khó quản lý khi form lớn
  • 🔥 Validation logic lộn xộn

Approach 2: React Hook Form

jsx
function RegistrationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => {
    console.log(data); // All form values!
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: 'Email required' })} />
      {errors.email && <span>{errors.email.message}</span>}

      <input {...register('password', { required: true, minLength: 6 })} />
      {errors.password && <span>Password must be 6+ characters</span>}

      {/* 8 more fields - same pattern! */}

      <button>Submit</button>
    </form>
  );
}

Lợi ích:

  • ✅ Ít code hơn (90% less boilerplate)
  • ✅ Better performance (ít re-renders)
  • ✅ Built-in validation
  • ✅ Easy to scale

1.2 Giải Pháp - React Hook Form

React Hook Form là library để quản lý forms với:

  • Uncontrolled components approach (ref-based)
  • Minimal re-renders (chỉ re-render khi cần)
  • Built-in validation (không cần thư viện khác)
  • TypeScript support
  • Small bundle size (~8KB)

Core concepts:

  1. useForm() - Hook chính
  2. register() - Đăng ký input
  3. handleSubmit() - Handle submit
  4. formState - Form state (errors, isDirty, isValid, etc.)

1.3 Mental Model

CONTROLLED vs UNCONTROLLED FORMS:

CONTROLLED (Traditional React):
┌─────────────────────────────────┐
│ Component State (useState)      │
│ ┌─────────────────────────────┐ │
│ │ value = "abc"               │ │
│ └─────────────────────────────┘ │
│           ↓↑ (sync)             │
│ ┌─────────────────────────────┐ │
│ │ <input value={value} />     │ │
│ │        onChange={...}        │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
Mỗi keystroke → setState → re-render
⚠️ Performance issue với large forms

UNCONTROLLED (React Hook Form):
┌─────────────────────────────────┐
│ DOM (Native Input)              │
│ ┌─────────────────────────────┐ │
│ │ <input ref={register} />    │ │
│ │ value lives in DOM          │ │
│ └─────────────────────────────┘ │
│           ↓ (on submit)         │
│ ┌─────────────────────────────┐ │
│ │ React Hook Form             │ │
│ │ reads value via ref         │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
Typing → NO re-render
Submit → read all values at once
✅ Better performance

Analogy:
- Controlled = Micromanager (theo dõi mọi thứ)
- Uncontrolled = Trust & verify (để tự do, check khi cần)

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

"React Hook Form chỉ cho uncontrolled components" → Sai! Có thể dùng với controlled components qua watch()setValue().

"Uncontrolled = không control được" → Sai! Vẫn control được, chỉ khác cách. Control qua ref thay vì state.

"React Hook Form thay thế tất cả useState cho forms" → Không hẳn. Simple forms (1-2 fields) có thể vẫn dùng useState.

"Không re-render = không thấy errors" → Sai! Errors vẫn hiển thị, RHF re-render khi errors thay đổi.


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

Demo 1: Basic Form - So sánh Approaches ⭐

Login form: Manual vs React Hook Form

💡 Code Example
jsx
/**
 * Login Form - Comparing Manual vs React Hook Form
 *
 * Demo shows:
 * - Manual approach (useState)
 * - React Hook Form approach
 * - Performance difference
 */

import { useState } from 'react';
import { useForm } from 'react-hook-form';

// ❌ MANUAL APPROACH - Traditional way
function LoginFormManual() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [renderCount, setRenderCount] = useState(0);

  // Track re-renders
  useState(() => {
    setRenderCount((prev) => prev + 1);
  });

  const validateEmail = (value) => {
    if (!value) return 'Email is required';
    if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
    return '';
  };

  const validatePassword = (value) => {
    if (!value) return 'Password is required';
    if (value.length < 6) return 'Password must be at least 6 characters';
    return '';
  };

  const handleEmailChange = (e) => {
    const value = e.target.value;
    setEmail(value);

    // Validate on change
    const error = validateEmail(value);
    setErrors((prev) => ({ ...prev, email: error }));
  };

  const handlePasswordChange = (e) => {
    const value = e.target.value;
    setPassword(value);

    const error = validatePassword(value);
    setErrors((prev) => ({ ...prev, password: error }));
  };

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

    const emailError = validateEmail(email);
    const passwordError = validatePassword(password);

    if (emailError || passwordError) {
      setErrors({ email: emailError, password: passwordError });
      return;
    }

    console.log('Manual form submitted:', { email, password });
    alert('Login successful!');
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ padding: '20px', border: '2px solid orange' }}
    >
      <h3>Manual Approach (useState)</h3>
      <p style={{ color: 'orange' }}>Render count: {renderCount}</p>

      <div style={{ marginBottom: '16px' }}>
        <label
          htmlFor='manual-email'
          style={{ display: 'block', marginBottom: '4px' }}
        >
          Email
        </label>
        <input
          id='manual-email'
          type='email'
          value={email}
          onChange={handleEmailChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.email ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.email && (
          <span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>
        )}
      </div>

      <div style={{ marginBottom: '16px' }}>
        <label
          htmlFor='manual-password'
          style={{ display: 'block', marginBottom: '4px' }}
        >
          Password
        </label>
        <input
          id='manual-password'
          type='password'
          value={password}
          onChange={handlePasswordChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.password ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.password && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.password}
          </span>
        )}
      </div>

      <button
        type='submit'
        style={{
          padding: '10px 20px',
          backgroundColor: '#ff9800',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Login (Manual)
      </button>
    </form>
  );
}

// ✅ REACT HOOK FORM APPROACH - Modern way
function LoginFormRHF() {
  const [renderCount, setRenderCount] = useState(0);

  // Track re-renders
  useState(() => {
    setRenderCount((prev) => prev + 1);
  });

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: 'onChange', // Validate on change
  });

  const onSubmit = (data) => {
    console.log('RHF form submitted:', data);
    alert('Login successful!');
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      style={{ padding: '20px', border: '2px solid green' }}
    >
      <h3>React Hook Form</h3>
      <p style={{ color: 'green' }}>Render count: {renderCount}</p>

      <div style={{ marginBottom: '16px' }}>
        <label
          htmlFor='rhf-email'
          style={{ display: 'block', marginBottom: '4px' }}
        >
          Email
        </label>
        <input
          id='rhf-email'
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: 'Email is invalid',
            },
          })}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.email ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.email && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.email.message}
          </span>
        )}
      </div>

      <div style={{ marginBottom: '16px' }}>
        <label
          htmlFor='rhf-password'
          style={{ display: 'block', marginBottom: '4px' }}
        >
          Password
        </label>
        <input
          id='rhf-password'
          type='password'
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 6,
              message: 'Password must be at least 6 characters',
            },
          })}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.password ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.password && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.password.message}
          </span>
        )}
      </div>

      <button
        type='submit'
        style={{
          padding: '10px 20px',
          backgroundColor: '#4caf50',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Login (RHF)
      </button>
    </form>
  );
}

// Compare side by side
function App() {
  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
      <h1>Login Form Comparison</h1>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: '20px',
          marginTop: '20px',
        }}
      >
        <LoginFormManual />
        <LoginFormRHF />
      </div>

      <div
        style={{
          marginTop: '40px',
          padding: '20px',
          backgroundColor: '#f5f5f5',
          borderRadius: '8px',
        }}
      >
        <h3>📊 Observe the differences:</h3>
        <ul>
          <li>
            <strong>Render Count:</strong>
            <ul>
              <li>Manual: Increases on EVERY keystroke</li>
              <li>RHF: Only increases on validation errors (much less!)</li>
            </ul>
          </li>
          <li>
            <strong>Code Amount:</strong>
            <ul>
              <li>Manual: ~80 lines (lots of boilerplate)</li>
              <li>RHF: ~50 lines (cleaner!)</li>
            </ul>
          </li>
          <li>
            <strong>Validation:</strong>
            <ul>
              <li>Manual: Write validation functions manually</li>
              <li>RHF: Built-in validation rules</li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
  );
}

/*
Key Takeaways:

Manual Approach:
❌많은 re-renders (performance issue)
❌ Lots of boilerplate
❌ Manual validation logic
❌ Hard to scale

React Hook Form:
✅ Minimal re-renders (better performance)
✅ Less code
✅ Built-in validation
✅ Easy to scale

When to use each:
- Manual: Very simple forms (1-2 fields)
- RHF: Medium to large forms (3+ fields)
*/

Demo 2: Validation Rules ⭐⭐

All built-in validation options

💡 Code Example
jsx
/**
 * React Hook Form - Validation Rules Demo
 *
 * Shows all built-in validation options:
 * - required
 * - minLength / maxLength
 * - min / max (numbers)
 * - pattern (regex)
 * - validate (custom function)
 */

import { useForm } from 'react-hook-form';

function ValidationDemo() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
  } = useForm({
    mode: 'onBlur', // Validate on blur
  });

  const password = watch('password'); // Watch password for confirm validation

  const onSubmit = (data) => {
    console.log('Form data:', data);
    alert('Form submitted successfully!');
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      style={{ maxWidth: '500px', padding: '20px' }}
    >
      <h2>Validation Rules Demo</h2>

      {/* 1. REQUIRED */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          1. Required Field
        </label>
        <input
          {...register('username', {
            required: 'Username is required',
          })}
          placeholder='Enter username'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.username ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.username && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.username.message}
          </span>
        )}
      </div>

      {/* 2. MIN/MAX LENGTH */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          2. Min/Max Length (6-20 chars)
        </label>
        <input
          type='password'
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 6,
              message: 'Password must be at least 6 characters',
            },
            maxLength: {
              value: 20,
              message: 'Password must not exceed 20 characters',
            },
          })}
          placeholder='Enter password'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.password ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.password && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.password.message}
          </span>
        )}
      </div>

      {/* 3. PATTERN (Regex) */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          3. Pattern - Email Format
        </label>
        <input
          type='email'
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: 'Invalid email address',
            },
          })}
          placeholder='user@example.com'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.email ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.email && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.email.message}
          </span>
        )}
      </div>

      {/* 4. MIN/MAX (Numbers) */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          4. Min/Max Value - Age (18-100)
        </label>
        <input
          type='number'
          {...register('age', {
            required: 'Age is required',
            min: {
              value: 18,
              message: 'You must be at least 18 years old',
            },
            max: {
              value: 100,
              message: 'Age must not exceed 100',
            },
          })}
          placeholder='Enter age'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.age ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.age && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.age.message}
          </span>
        )}
      </div>

      {/* 5. VALIDATE (Custom function) */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          5. Custom Validation - Confirm Password
        </label>
        <input
          type='password'
          {...register('confirmPassword', {
            required: 'Please confirm password',
            validate: (value) => value === password || 'Passwords do not match',
          })}
          placeholder='Confirm password'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.confirmPassword ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.confirmPassword && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.confirmPassword.message}
          </span>
        )}
      </div>

      {/* 6. MULTIPLE VALIDATIONS */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          6. Multiple Custom Validations - Username
        </label>
        <input
          {...register('customUsername', {
            required: 'Username is required',
            validate: {
              noSpaces: (value) =>
                !/\s/.test(value) || 'Username cannot contain spaces',
              noSpecialChars: (value) =>
                /^[a-zA-Z0-9_]+$/.test(value) ||
                'Only letters, numbers, and underscore allowed',
              minLength: (value) =>
                value.length >= 3 || 'Username must be at least 3 characters',
            },
          })}
          placeholder='username_123'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.customUsername ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.customUsername && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.customUsername.message}
          </span>
        )}
      </div>

      {/* 7. CONDITIONAL VALIDATION */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}
        >
          7. Conditional - Phone (required if age &lt; 18)
        </label>
        <input
          {...register('phone', {
            validate: (value) => {
              const age = watch('age');
              if (age && age < 18 && !value) {
                return 'Phone is required for users under 18';
              }
              return true;
            },
          })}
          placeholder='Phone number'
          style={{
            width: '100%',
            padding: '8px',
            border: errors.phone ? '2px solid red' : '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
        {errors.phone && (
          <span style={{ color: 'red', fontSize: '14px' }}>
            {errors.phone.message}
          </span>
        )}
      </div>

      <button
        type='submit'
        style={{
          width: '100%',
          padding: '12px',
          backgroundColor: '#4caf50',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          fontSize: '16px',
          cursor: 'pointer',
        }}
      >
        Submit
      </button>

      <div
        style={{
          marginTop: '20px',
          padding: '16px',
          backgroundColor: '#f5f5f5',
          borderRadius: '4px',
        }}
      >
        <h4>📋 Validation Rules Summary:</h4>
        <ol style={{ margin: '8px 0', paddingLeft: '20px' }}>
          <li>
            <code>required</code> - Field cannot be empty
          </li>
          <li>
            <code>minLength / maxLength</code> - String length limits
          </li>
          <li>
            <code>pattern</code> - Regex validation
          </li>
          <li>
            <code>min / max</code> - Number range
          </li>
          <li>
            <code>validate</code> - Custom function (single)
          </li>
          <li>
            <code>validate: {`{}`}</code> - Multiple custom functions
          </li>
          <li>
            Conditional validation using <code>watch()</code>
          </li>
        </ol>
      </div>
    </form>
  );
}

/*
Validation Rules Reference:

1. required: 'Message' | boolean
2. minLength: { value: number, message: string }
3. maxLength: { value: number, message: string }
4. min: { value: number, message: string }
5. max: { value: number, message: string }
6. pattern: { value: RegExp, message: string }
7. validate: (value) => boolean | string
8. validate: { 
     rule1: (value) => boolean | string,
     rule2: (value) => boolean | string
   }

Tips:
- Use mode: 'onBlur' for better UX (validate after blur)
- Use mode: 'onChange' for real-time validation
- Use mode: 'onSubmit' (default) for validation only on submit
- Custom validate can access other field values via watch()
*/

Demo 3: Form State & Features ⭐⭐⭐

Using formState and other RHF features

💡 Code Example
jsx
/**
 * React Hook Form - Form State & Features
 *
 * Demonstrates:
 * - formState properties
 * - reset()
 * - watch()
 * - setValue()
 * - Disabled state
 */

import { useForm } from 'react-hook-form';
import { useState } from 'react';

function FormStateDemo() {
  const {
    register,
    handleSubmit,
    formState: {
      errors,
      isDirty,
      isValid,
      isSubmitting,
      touchedFields,
      dirtyFields,
    },
    watch,
    reset,
    setValue,
  } = useForm({
    mode: 'onChange', // Validate on change to see isValid
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      subscribe: false,
    },
  });

  const [submittedData, setSubmittedData] = useState(null);

  // Watch specific field
  const watchFirstName = watch('firstName');

  // Watch all fields
  const watchAllFields = watch();

  const onSubmit = async (data) => {
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 2000));

    console.log('Submitted:', data);
    setSubmittedData(data);
  };

  return (
    <div style={{ maxWidth: '600px', padding: '20px' }}>
      <h2>Form State & Features Demo</h2>

      {/* Form State Display */}
      <div
        style={{
          marginBottom: '20px',
          padding: '16px',
          backgroundColor: '#e3f2fd',
          borderRadius: '8px',
        }}
      >
        <h3>📊 Form State:</h3>
        <ul style={{ margin: 0, paddingLeft: '20px' }}>
          <li>
            <strong>isDirty:</strong> {isDirty ? '✅ Yes' : '❌ No'}
            <small> (any field changed?)</small>
          </li>
          <li>
            <strong>isValid:</strong> {isValid ? '✅ Yes' : '❌ No'}
            <small> (all validations pass?)</small>
          </li>
          <li>
            <strong>isSubmitting:</strong> {isSubmitting ? '⏳ Yes' : '❌ No'}
            <small> (form being submitted?)</small>
          </li>
          <li>
            <strong>touchedFields:</strong>{' '}
            {Object.keys(touchedFields).join(', ') || 'none'}
          </li>
          <li>
            <strong>dirtyFields:</strong>{' '}
            {Object.keys(dirtyFields).join(', ') || 'none'}
          </li>
        </ul>
      </div>

      {/* Live Watch */}
      <div
        style={{
          marginBottom: '20px',
          padding: '16px',
          backgroundColor: '#fff3e0',
          borderRadius: '8px',
        }}
      >
        <h3>👁️ Watch Values:</h3>
        <p>
          <strong>First Name:</strong> {watchFirstName || '(empty)'}
        </p>
        <pre
          style={{
            backgroundColor: '#f5f5f5',
            padding: '8px',
            borderRadius: '4px',
            overflow: 'auto',
            fontSize: '12px',
          }}
        >
          {JSON.stringify(watchAllFields, null, 2)}
        </pre>
      </div>

      <form onSubmit={handleSubmit(onSubmit)}>
        {/* First Name */}
        <div style={{ marginBottom: '16px' }}>
          <label
            style={{
              display: 'block',
              marginBottom: '4px',
              fontWeight: 'bold',
            }}
          >
            First Name *
          </label>
          <input
            {...register('firstName', {
              required: 'First name is required',
              minLength: {
                value: 2,
                message: 'Must be at least 2 characters',
              },
            })}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.firstName ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.firstName && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.firstName.message}
            </span>
          )}
        </div>

        {/* Last Name */}
        <div style={{ marginBottom: '16px' }}>
          <label
            style={{
              display: 'block',
              marginBottom: '4px',
              fontWeight: 'bold',
            }}
          >
            Last Name *
          </label>
          <input
            {...register('lastName', {
              required: 'Last name is required',
            })}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.lastName ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.lastName && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.lastName.message}
            </span>
          )}
        </div>

        {/* Email */}
        <div style={{ marginBottom: '16px' }}>
          <label
            style={{
              display: 'block',
              marginBottom: '4px',
              fontWeight: 'bold',
            }}
          >
            Email *
          </label>
          <input
            type='email'
            {...register('email', {
              required: 'Email is required',
              pattern: {
                value: /\S+@\S+\.\S+/,
                message: 'Invalid email',
              },
            })}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.email ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.email && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.email.message}
            </span>
          )}
        </div>

        {/* Checkbox */}
        <div style={{ marginBottom: '16px' }}>
          <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
            <input
              type='checkbox'
              {...register('subscribe')}
            />
            <span>Subscribe to newsletter</span>
          </label>
        </div>

        {/* Buttons */}
        <div style={{ display: 'flex', gap: '8px' }}>
          <button
            type='submit'
            disabled={isSubmitting || !isValid}
            style={{
              flex: 1,
              padding: '12px',
              backgroundColor: isSubmitting || !isValid ? '#ccc' : '#4caf50',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: isSubmitting || !isValid ? 'not-allowed' : 'pointer',
            }}
          >
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>

          <button
            type='button'
            onClick={() => reset()}
            disabled={!isDirty}
            style={{
              flex: 1,
              padding: '12px',
              backgroundColor: !isDirty ? '#ccc' : '#f44336',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: !isDirty ? 'not-allowed' : 'pointer',
            }}
          >
            Reset
          </button>

          <button
            type='button'
            onClick={() => {
              setValue('firstName', 'John', {
                shouldValidate: true,
                shouldDirty: true,
              });
              setValue('lastName', 'Doe', {
                shouldValidate: true,
                shouldDirty: true,
              });
              setValue('email', 'john@example.com', {
                shouldValidate: true,
                shouldDirty: true,
              });
            }}
            style={{
              flex: 1,
              padding: '12px',
              backgroundColor: '#2196f3',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            Fill Demo Data
          </button>
        </div>
      </form>

      {/* Submitted Data Display */}
      {submittedData && (
        <div
          style={{
            marginTop: '20px',
            padding: '16px',
            backgroundColor: '#e8f5e9',
            borderRadius: '8px',
          }}
        >
          <h3>✅ Form Submitted Successfully!</h3>
          <pre
            style={{
              backgroundColor: '#f5f5f5',
              padding: '12px',
              borderRadius: '4px',
              overflow: 'auto',
            }}
          >
            {JSON.stringify(submittedData, null, 2)}
          </pre>
        </div>
      )}

      {/* Documentation */}
      <div
        style={{
          marginTop: '20px',
          padding: '16px',
          backgroundColor: '#f5f5f5',
          borderRadius: '8px',
        }}
      >
        <h4>📚 Features Used:</h4>
        <ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
          <li>
            <code>formState.isDirty</code> - Any field changed?
          </li>
          <li>
            <code>formState.isValid</code> - All validations pass?
          </li>
          <li>
            <code>formState.isSubmitting</code> - Currently submitting?
          </li>
          <li>
            <code>watch()</code> - Get current field values
          </li>
          <li>
            <code>reset()</code> - Reset form to default
          </li>
          <li>
            <code>setValue()</code> - Programmatically set value
          </li>
        </ul>
      </div>
    </div>
  );
}

/*
formState Properties:

- isDirty: boolean - Any field modified?
- isValid: boolean - All validations pass?
- isSubmitting: boolean - Form being submitted?
- isSubmitted: boolean - Form was submitted?
- touchedFields: object - Fields that were focused
- dirtyFields: object - Fields that were changed
- errors: object - Validation errors

Methods:

- watch(name?) - Watch field values
- reset(values?) - Reset form
- setValue(name, value, options?) - Set value programmatically
- getValues() - Get all form values
- trigger(name?) - Trigger validation manually

Options for setValue:
- shouldValidate: boolean - Trigger validation?
- shouldDirty: boolean - Mark as dirty?
- shouldTouch: boolean - Mark as touched?
*/

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

⭐ Bài 1: Simple Contact Form (15 phút)

🎯 Mục tiêu: Làm quen với RHF basic API

jsx
/**
 * 🎯 Mục tiêu: Tạo contact form với React Hook Form
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Zod, Yup (chưa học)
 *
 * Requirements:
 * 1. Fields: name, email, message
 * 2. Validation:
 *    - All fields required
 *    - Email format validation
 *    - Message min length: 10 chars
 * 3. Show errors on blur
 * 4. Disable submit nếu có errors
 * 5. Reset form sau khi submit thành công
 *
 * 💡 Gợi ý:
 * - Dùng mode: 'onBlur'
 * - Dùng formState.isValid
 * - Dùng reset() sau submit
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
// TODO: Implement ContactForm với React Hook Form
💡 Solution
jsx
/**
 * Contact Form - React Hook Form Solution
 */

import { useForm } from 'react-hook-form';
import { useState } from 'react';

function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    reset,
  } = useForm({
    mode: 'onBlur', // Validate on blur
  });

  const [submitStatus, setSubmitStatus] = useState(null);

  const onSubmit = (data) => {
    console.log('Contact form submitted:', data);

    // Simulate API call
    setTimeout(() => {
      setSubmitStatus('success');
      reset(); // Reset form after successful submit

      // Clear success message after 3s
      setTimeout(() => setSubmitStatus(null), 3000);
    }, 500);
  };

  return (
    <div style={{ maxWidth: '500px', padding: '20px' }}>
      <h2>Contact Us</h2>

      {submitStatus === 'success' && (
        <div
          style={{
            padding: '12px',
            backgroundColor: '#d4edda',
            border: '1px solid #c3e6cb',
            borderRadius: '4px',
            marginBottom: '16px',
            color: '#155724',
          }}
        >
          ✅ Message sent successfully!
        </div>
      )}

      <form onSubmit={handleSubmit(onSubmit)}>
        {/* Name */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='name'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Name *
          </label>
          <input
            id='name'
            {...register('name', {
              required: 'Name is required',
            })}
            placeholder='Your name'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.name ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.name && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.name.message}
            </span>
          )}
        </div>

        {/* Email */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='email'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Email *
          </label>
          <input
            id='email'
            type='email'
            {...register('email', {
              required: 'Email is required',
              pattern: {
                value: /\S+@\S+\.\S+/,
                message: 'Please enter a valid email address',
              },
            })}
            placeholder='your@email.com'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.email ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.email && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.email.message}
            </span>
          )}
        </div>

        {/* Message */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='message'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Message *
          </label>
          <textarea
            id='message'
            {...register('message', {
              required: 'Message is required',
              minLength: {
                value: 10,
                message: 'Message must be at least 10 characters',
              },
            })}
            placeholder='Your message...'
            rows={4}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.message ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
              fontFamily: 'inherit',
              resize: 'vertical',
            }}
          />
          {errors.message && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.message.message}
            </span>
          )}
        </div>

        {/* Submit */}
        <button
          type='submit'
          disabled={!isValid}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: !isValid ? '#ccc' : '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: !isValid ? 'not-allowed' : 'pointer',
          }}
        >
          Send Message
        </button>
      </form>
    </div>
  );
}

/*
Kết quả:
✅ Basic RHF setup
✅ Validation on blur
✅ Error messages
✅ Disabled state
✅ Form reset after submit
*/

⭐⭐ Bài 2: Registration Form với Password Strength (25 phút)

🎯 Mục tiêu: Advanced validation patterns

jsx
/**
 * 🎯 Mục tiêu: Registration form với custom validation
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: User registration với password strength indicator
 *
 * Requirements:
 * 1. Fields: username, email, password, confirmPassword
 * 2. Username validation:
 *    - Required
 *    - Min 3 chars
 *    - No spaces
 *    - Alphanumeric + underscore only
 * 3. Password validation:
 *    - Required
 *    - Min 8 chars
 *    - Must contain: uppercase, lowercase, number
 * 4. Password strength indicator:
 *    - Weak (< 8 chars)
 *    - Medium (8+ chars, 2/3 requirements)
 *    - Strong (8+ chars, all requirements)
 * 5. Confirm password must match
 * 6. Show live validation feedback
 *
 * 💡 Gợi ý:
 * - Use validate object for multiple rules
 * - Use watch() for password strength
 * - Use custom validation function
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
// TODO: Implement RegistrationForm
💡 Solution
jsx
/**
 * Registration Form with Password Strength
 */

import { useForm } from 'react-hook-form';
import { useState, useMemo } from 'react';

// Password strength calculator
function calculatePasswordStrength(password) {
  if (!password) return { strength: 'none', score: 0 };

  let score = 0;
  const checks = {
    length: password.length >= 8,
    uppercase: /[A-Z]/.test(password),
    lowercase: /[a-z]/.test(password),
    number: /[0-9]/.test(password),
    special: /[!@#$%^&*]/.test(password),
  };

  if (checks.length) score++;
  if (checks.uppercase) score++;
  if (checks.lowercase) score++;
  if (checks.number) score++;
  if (checks.special) score++;

  let strength = 'weak';
  if (score >= 4) strength = 'strong';
  else if (score >= 2) strength = 'medium';

  return { strength, score, checks };
}

function RegistrationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    watch,
    reset,
  } = useForm({
    mode: 'onChange', // Real-time validation
  });

  const [submitStatus, setSubmitStatus] = useState(null);

  const password = watch('password');
  const passwordStrength = useMemo(
    () => calculatePasswordStrength(password),
    [password],
  );

  const onSubmit = async (data) => {
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1500));

    console.log('Registration data:', data);
    setSubmitStatus('success');
    reset();

    setTimeout(() => setSubmitStatus(null), 3000);
  };

  // Strength indicator colors
  const strengthColors = {
    none: '#ccc',
    weak: '#f44336',
    medium: '#ff9800',
    strong: '#4caf50',
  };

  return (
    <div style={{ maxWidth: '500px', padding: '20px' }}>
      <h2>Create Account</h2>

      {submitStatus === 'success' && (
        <div
          style={{
            padding: '12px',
            backgroundColor: '#d4edda',
            border: '1px solid #c3e6cb',
            borderRadius: '4px',
            marginBottom: '16px',
            color: '#155724',
          }}
        >
          ✅ Account created successfully!
        </div>
      )}

      <form onSubmit={handleSubmit(onSubmit)}>
        {/* Username */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='username'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Username *
          </label>
          <input
            id='username'
            {...register('username', {
              required: 'Username is required',
              minLength: {
                value: 3,
                message: 'Username must be at least 3 characters',
              },
              validate: {
                noSpaces: (value) =>
                  !/\s/.test(value) || 'Username cannot contain spaces',
                alphanumeric: (value) =>
                  /^[a-zA-Z0-9_]+$/.test(value) ||
                  'Only letters, numbers, and underscore allowed',
              },
            })}
            placeholder='username_123'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.username ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.username && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.username.message}
            </span>
          )}
        </div>

        {/* Email */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='email'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Email *
          </label>
          <input
            id='email'
            type='email'
            {...register('email', {
              required: 'Email is required',
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: 'Invalid email address',
              },
            })}
            placeholder='your@email.com'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.email ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.email && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.email.message}
            </span>
          )}
        </div>

        {/* Password */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='password'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Password *
          </label>
          <input
            id='password'
            type='password'
            {...register('password', {
              required: 'Password is required',
              minLength: {
                value: 8,
                message: 'Password must be at least 8 characters',
              },
              validate: {
                hasUppercase: (value) =>
                  /[A-Z]/.test(value) || 'Must contain uppercase letter',
                hasLowercase: (value) =>
                  /[a-z]/.test(value) || 'Must contain lowercase letter',
                hasNumber: (value) =>
                  /[0-9]/.test(value) || 'Must contain number',
              },
            })}
            placeholder='********'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.password ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.password && (
            <span style={{ color: 'red', fontSize: '14px', display: 'block' }}>
              {errors.password.message}
            </span>
          )}

          {/* Password Strength Indicator */}
          {password && (
            <div style={{ marginTop: '8px' }}>
              <div style={{ display: 'flex', gap: '4px', marginBottom: '4px' }}>
                <div
                  style={{
                    flex: 1,
                    height: '4px',
                    backgroundColor:
                      passwordStrength.score >= 1
                        ? strengthColors[passwordStrength.strength]
                        : '#e0e0e0',
                    borderRadius: '2px',
                  }}
                />
                <div
                  style={{
                    flex: 1,
                    height: '4px',
                    backgroundColor:
                      passwordStrength.score >= 2
                        ? strengthColors[passwordStrength.strength]
                        : '#e0e0e0',
                    borderRadius: '2px',
                  }}
                />
                <div
                  style={{
                    flex: 1,
                    height: '4px',
                    backgroundColor:
                      passwordStrength.score >= 3
                        ? strengthColors[passwordStrength.strength]
                        : '#e0e0e0',
                    borderRadius: '2px',
                  }}
                />
                <div
                  style={{
                    flex: 1,
                    height: '4px',
                    backgroundColor:
                      passwordStrength.score >= 4
                        ? strengthColors[passwordStrength.strength]
                        : '#e0e0e0',
                    borderRadius: '2px',
                  }}
                />
              </div>
              <span
                style={{
                  fontSize: '12px',
                  color: strengthColors[passwordStrength.strength],
                  textTransform: 'capitalize',
                }}
              >
                {passwordStrength.strength} password
              </span>

              <div
                style={{ fontSize: '12px', marginTop: '4px', color: '#666' }}
              >
                ✓ Requirements:
                <ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
                  <li
                    style={{
                      color: passwordStrength.checks.length
                        ? 'green'
                        : 'inherit',
                    }}
                  >
                    {passwordStrength.checks.length ? '✓' : '○'} 8+ characters
                  </li>
                  <li
                    style={{
                      color: passwordStrength.checks.uppercase
                        ? 'green'
                        : 'inherit',
                    }}
                  >
                    {passwordStrength.checks.uppercase ? '✓' : '○'} Uppercase
                    letter
                  </li>
                  <li
                    style={{
                      color: passwordStrength.checks.lowercase
                        ? 'green'
                        : 'inherit',
                    }}
                  >
                    {passwordStrength.checks.lowercase ? '✓' : '○'} Lowercase
                    letter
                  </li>
                  <li
                    style={{
                      color: passwordStrength.checks.number
                        ? 'green'
                        : 'inherit',
                    }}
                  >
                    {passwordStrength.checks.number ? '✓' : '○'} Number
                  </li>
                </ul>
              </div>
            </div>
          )}
        </div>

        {/* Confirm Password */}
        <div style={{ marginBottom: '16px' }}>
          <label
            htmlFor='confirmPassword'
            style={{ display: 'block', marginBottom: '4px' }}
          >
            Confirm Password *
          </label>
          <input
            id='confirmPassword'
            type='password'
            {...register('confirmPassword', {
              required: 'Please confirm your password',
              validate: (value) =>
                value === password || 'Passwords do not match',
            })}
            placeholder='********'
            style={{
              width: '100%',
              padding: '8px',
              border: errors.confirmPassword
                ? '2px solid red'
                : '1px solid #ccc',
              borderRadius: '4px',
            }}
          />
          {errors.confirmPassword && (
            <span style={{ color: 'red', fontSize: '14px' }}>
              {errors.confirmPassword.message}
            </span>
          )}
        </div>

        {/* Submit */}
        <button
          type='submit'
          disabled={isSubmitting}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isSubmitting ? '#ccc' : '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: isSubmitting ? 'not-allowed' : 'pointer',
          }}
        >
          {isSubmitting ? 'Creating Account...' : 'Create Account'}
        </button>
      </form>
    </div>
  );
}

/*
Kết quả:
✅ Multiple validation rules per field
✅ Live password strength indicator
✅ Password match validation
✅ Custom validation functions
✅ Real-time feedback
✅ Good UX with visual indicators
*/

⭐⭐⭐ Bài 3: Dynamic Form Fields (40 phút)

🎯 Mục tiêu: Array fields management

jsx
/**
 * 🎯 Mục tiêu: Form với dynamic array fields
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là HR, tôi muốn thêm/xóa nhiều work experiences
 * để có thể track hết quá trình làm việc của candidate"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Add new work experience entry
 * - [ ] Remove work experience entry
 * - [ ] Each entry có: company, position, startDate, endDate
 * - [ ] Validation: endDate > startDate
 * - [ ] At least 1 work experience required
 * - [ ] Max 5 work experiences
 *
 * 🎨 Technical Constraints:
 * - Use useFieldArray từ React Hook Form
 * - Proper validation for each field
 * - Clear UX for add/remove
 *
 * 🚨 Edge Cases:
 * - Cannot remove last item
 * - Cannot add more than 5 items
 * - Date validation
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
// TODO: Implement WorkExperienceForm với useFieldArray
💡 Solution
jsx
/**
 * Work Experience Form - Dynamic Fields với useFieldArray
 */

import { useForm, useFieldArray } from 'react-hook-form';

function WorkExperienceForm() {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors },
    watch,
  } = useForm({
    defaultValues: {
      experiences: [
        {
          company: '',
          position: '',
          startDate: '',
          endDate: '',
        },
      ],
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'experiences',
  });

  const onSubmit = (data) => {
    console.log('Work experiences:', data);
    alert('Form submitted! Check console.');
  };

  // Validate end date > start date
  const validateEndDate = (endDate, index) => {
    const startDate = watch(`experiences.${index}.startDate`);
    if (!startDate || !endDate) return true;

    return (
      new Date(endDate) > new Date(startDate) ||
      'End date must be after start date'
    );
  };

  return (
    <div style={{ maxWidth: '700px', padding: '20px' }}>
      <h2>Work Experience</h2>
      <p style={{ color: '#666' }}>
        Add your work experiences (minimum 1, maximum 5)
      </p>

      <form onSubmit={handleSubmit(onSubmit)}>
        {fields.map((field, index) => (
          <div
            key={field.id}
            style={{
              marginBottom: '24px',
              padding: '16px',
              border: '1px solid #e0e0e0',
              borderRadius: '8px',
              backgroundColor: '#f9f9f9',
            }}
          >
            <div
              style={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                marginBottom: '12px',
              }}
            >
              <h3 style={{ margin: 0 }}>Experience #{index + 1}</h3>

              {fields.length > 1 && (
                <button
                  type='button'
                  onClick={() => remove(index)}
                  style={{
                    padding: '6px 12px',
                    backgroundColor: '#f44336',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer',
                    fontSize: '14px',
                  }}
                >
                  Remove
                </button>
              )}
            </div>

            <div
              style={{
                display: 'grid',
                gridTemplateColumns: '1fr 1fr',
                gap: '12px',
              }}
            >
              {/* Company */}
              <div>
                <label
                  style={{
                    display: 'block',
                    marginBottom: '4px',
                    fontSize: '14px',
                  }}
                >
                  Company *
                </label>
                <input
                  {...register(`experiences.${index}.company`, {
                    required: 'Company name is required',
                  })}
                  placeholder='Company name'
                  style={{
                    width: '100%',
                    padding: '8px',
                    border: errors.experiences?.[index]?.company
                      ? '2px solid red'
                      : '1px solid #ccc',
                    borderRadius: '4px',
                  }}
                />
                {errors.experiences?.[index]?.company && (
                  <span style={{ color: 'red', fontSize: '12px' }}>
                    {errors.experiences[index].company.message}
                  </span>
                )}
              </div>

              {/* Position */}
              <div>
                <label
                  style={{
                    display: 'block',
                    marginBottom: '4px',
                    fontSize: '14px',
                  }}
                >
                  Position *
                </label>
                <input
                  {...register(`experiences.${index}.position`, {
                    required: 'Position is required',
                  })}
                  placeholder='Job title'
                  style={{
                    width: '100%',
                    padding: '8px',
                    border: errors.experiences?.[index]?.position
                      ? '2px solid red'
                      : '1px solid #ccc',
                    borderRadius: '4px',
                  }}
                />
                {errors.experiences?.[index]?.position && (
                  <span style={{ color: 'red', fontSize: '12px' }}>
                    {errors.experiences[index].position.message}
                  </span>
                )}
              </div>

              {/* Start Date */}
              <div>
                <label
                  style={{
                    display: 'block',
                    marginBottom: '4px',
                    fontSize: '14px',
                  }}
                >
                  Start Date *
                </label>
                <input
                  type='date'
                  {...register(`experiences.${index}.startDate`, {
                    required: 'Start date is required',
                  })}
                  style={{
                    width: '100%',
                    padding: '8px',
                    border: errors.experiences?.[index]?.startDate
                      ? '2px solid red'
                      : '1px solid #ccc',
                    borderRadius: '4px',
                  }}
                />
                {errors.experiences?.[index]?.startDate && (
                  <span style={{ color: 'red', fontSize: '12px' }}>
                    {errors.experiences[index].startDate.message}
                  </span>
                )}
              </div>

              {/* End Date */}
              <div>
                <label
                  style={{
                    display: 'block',
                    marginBottom: '4px',
                    fontSize: '14px',
                  }}
                >
                  End Date *
                </label>
                <input
                  type='date'
                  {...register(`experiences.${index}.endDate`, {
                    required: 'End date is required',
                    validate: (value) => validateEndDate(value, index),
                  })}
                  style={{
                    width: '100%',
                    padding: '8px',
                    border: errors.experiences?.[index]?.endDate
                      ? '2px solid red'
                      : '1px solid #ccc',
                    borderRadius: '4px',
                  }}
                />
                {errors.experiences?.[index]?.endDate && (
                  <span style={{ color: 'red', fontSize: '12px' }}>
                    {errors.experiences[index].endDate.message}
                  </span>
                )}
              </div>
            </div>
          </div>
        ))}

        {/* Add Button */}
        <button
          type='button'
          onClick={() =>
            append({
              company: '',
              position: '',
              startDate: '',
              endDate: '',
            })
          }
          disabled={fields.length >= 5}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: fields.length >= 5 ? '#ccc' : '#2196f3',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: fields.length >= 5 ? 'not-allowed' : 'pointer',
            marginBottom: '16px',
          }}
        >
          ➕ Add Work Experience {fields.length >= 5 && '(Max 5 reached)'}
        </button>

        {/* Submit */}
        <button
          type='submit'
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: 'pointer',
          }}
        >
          Submit
        </button>
      </form>

      <div
        style={{
          marginTop: '20px',
          padding: '16px',
          backgroundColor: '#e3f2fd',
          borderRadius: '8px',
        }}
      >
        <h4>💡 Features Demonstrated:</h4>
        <ul style={{ margin: '8px 0', paddingLeft: '20px', fontSize: '14px' }}>
          <li>
            Dynamic array fields với <code>useFieldArray</code>
          </li>
          <li>Add/Remove entries</li>
          <li>Individual field validation</li>
          <li>Cross-field validation (end date vs start date)</li>
          <li>Min/Max constraints (1-5 entries)</li>
          <li>
            Unique IDs với <code>field.id</code>
          </li>
        </ul>
      </div>
    </div>
  );
}

/*
useFieldArray API:

const { fields, append, remove, insert, update } = useFieldArray({
  control,
  name: 'arrayFieldName'
});

Methods:
- append(value) - Add to end
- prepend(value) - Add to start
- insert(index, value) - Insert at index
- remove(index) - Remove at index
- update(index, value) - Update at index
- replace(values) - Replace entire array

Important:
- Always use field.id as key (NOT index!)
- Default values needed in useForm()
- Proper nested error access: errors.array?.[index]?.field
*/

⭐⭐⭐⭐ Bài 4: Form Architecture Decision (60 phút)

🎯 Mục tiêu: Đưa ra architectural choices có lý do

jsx
/**
 * 🎯 Mục tiêu: Design form architecture cho Job Application
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Scenario: Job Application form với 3 sections:
 * - Personal Info (6 fields)
 * - Work Experience (dynamic array, max 5)
 * - Skills & Preferences (multiple checkboxes, text areas)
 *
 * Questions to answer:
 * 1. Single form vs Multi-step wizard?
 * 2. Validation timing: onChange, onBlur, or onSubmit?
 * 3. Error display strategy?
 * 4. Save draft strategy (no backend yet)?
 *
 * Nhiệm vụ:
 * 1. So sánh ít nhất 3 approaches
 * 2. Document pros/cons mỗi approach
 * 3. Chọn approach phù hợp nhất
 * 4. Viết ADR (Architecture Decision Record)
 *
 * ADR Template:
 * - Context: Vấn đề cần giải quyết
 * - Decision: Approach đã chọn
 * - Rationale: Tại sao chọn approach này
 * - Consequences: Trade-offs accepted
 * - Alternatives Considered: Các options khác
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * Implement solution theo design đã chọn
 *
 * 🧪 PHASE 3: Testing (10 phút)
 * - Manual testing checklist
 * - Edge cases verification
 */
💡 Solution
jsx
/**
 * Job Application Form - Architecture Decision
 *
 * ADR (Architecture Decision Record)
 * ================================
 *
 * Context:
 * Need to build job application form with:
 * - 15+ total fields across 3 sections
 * - Dynamic work experience entries
 * - Complex validation rules
 * - Good UX for long forms
 * - No backend yet (save draft to localStorage)
 *
 * Decision: Single-page form with sections + Smart validation
 *
 * Rationale:
 * 1. Single page better than multi-step because:
 *    - Users can see all requirements upfront
 *    - Easier to review before submit
 *    - Less state management complexity
 *    - No router needed (not learned yet)
 *
 * 2. Validation strategy: onBlur + onChange for errors
 *    - onBlur: First validation (less annoying)
 *    - onChange: Show errors after first blur (real-time feedback)
 *    - Better UX than onChange only (too aggressive)
 *
 * 3. Error display: Inline + Summary at top
 *    - Inline: Immediate feedback
 *    - Summary: Overview of all errors on submit
 *
 * 4. Save draft: localStorage on field blur
 *    - Auto-save every field change (onBlur)
 *    - Restore on mount
 *    - Simple, no backend needed
 *
 * Consequences (Trade-offs):
 * ✅ Pros:
 *    - Simple architecture
 *    - Good UX (see all fields)
 *    - Auto-save prevents data loss
 *    - Easy to implement
 *
 * ❌ Cons:
 *    - Long form might intimidate users
 *    - No progress indicator (vs multi-step)
 *    - localStorage limited to ~5MB
 *
 * Alternatives Considered:
 * 1. Multi-step wizard
 *    - Rejected: Need router (not learned), more complex state
 * 2. onChange validation only
 *    - Rejected: Too aggressive, bad UX
 * 3. onSubmit validation only
 *    - Rejected: Late feedback, users frustrated
 */

import { useForm } from 'react-hook-form';
import { useEffect, useState } from 'react';

const STORAGE_KEY = 'job_application_draft';

function JobApplicationForm() {
  const [errorSummary, setErrorSummary] = useState([]);

  // Load draft from localStorage
  const loadDraft = () => {
    try {
      const draft = localStorage.getItem(STORAGE_KEY);
      return draft ? JSON.parse(draft) : getDefaultValues();
    } catch {
      return getDefaultValues();
    }
  };

  const {
    register,
    handleSubmit,
    formState: { errors, touchedFields },
    watch,
    reset,
  } = useForm({
    mode: 'onTouched', // Validate after first blur
    defaultValues: loadDraft(),
  });

  // Auto-save to localStorage
  const formValues = watch();
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(formValues));
    }, 500); // Debounce 500ms

    return () => clearTimeout(timeoutId);
  }, [formValues]);

  const onSubmit = (data) => {
    console.log('Application submitted:', data);

    // Clear draft
    localStorage.removeItem(STORAGE_KEY);

    alert('Application submitted successfully!');
    reset(getDefaultValues());
    setErrorSummary([]);
  };

  const onError = (errors) => {
    // Collect all error messages for summary
    const messages = [];
    Object.entries(errors).forEach(([field, error]) => {
      if (error.message) {
        messages.push({ field, message: error.message });
      }
    });
    setErrorSummary(messages);

    // Scroll to top to show summary
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const clearDraft = () => {
    if (window.confirm('Clear saved draft?')) {
      localStorage.removeItem(STORAGE_KEY);
      reset(getDefaultValues());
    }
  };

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <h1>Job Application Form</h1>

      {/* Error Summary */}
      {errorSummary.length > 0 && (
        <div
          style={{
            padding: '16px',
            backgroundColor: '#ffebee',
            border: '1px solid #f44336',
            borderRadius: '8px',
            marginBottom: '24px',
          }}
        >
          <h3 style={{ margin: '0 0 8px 0', color: '#c62828' }}>
            ⚠️ Please fix {errorSummary.length} error(s):
          </h3>
          <ul style={{ margin: 0, paddingLeft: '20px' }}>
            {errorSummary.map((err, i) => (
              <li
                key={i}
                style={{ color: '#c62828' }}
              >
                <strong>{err.field}:</strong> {err.message}
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* Draft indicator */}
      <div
        style={{
          padding: '12px',
          backgroundColor: '#e3f2fd',
          borderRadius: '4px',
          marginBottom: '20px',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <span>💾 Draft auto-saved to your browser</span>
        <button
          type='button'
          onClick={clearDraft}
          style={{
            padding: '6px 12px',
            backgroundColor: '#f44336',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '14px',
          }}
        >
          Clear Draft
        </button>
      </div>

      <form onSubmit={handleSubmit(onSubmit, onError)}>
        {/* SECTION 1: Personal Information */}
        <section
          style={{
            marginBottom: '32px',
            padding: '20px',
            backgroundColor: '#f9f9f9',
            borderRadius: '8px',
          }}
        >
          <h2>Personal Information</h2>

          <div
            style={{
              display: 'grid',
              gridTemplateColumns: '1fr 1fr',
              gap: '16px',
            }}
          >
            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                First Name *
              </label>
              <input
                {...register('firstName', {
                  required: 'First name is required',
                  minLength: { value: 2, message: 'Min 2 characters' },
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.firstName ? '2px solid red' : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.firstName && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.firstName.message}
                </span>
              )}
            </div>

            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Last Name *
              </label>
              <input
                {...register('lastName', {
                  required: 'Last name is required',
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.lastName ? '2px solid red' : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.lastName && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.lastName.message}
                </span>
              )}
            </div>

            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Email *
              </label>
              <input
                type='email'
                {...register('email', {
                  required: 'Email is required',
                  pattern: {
                    value: /\S+@\S+\.\S+/,
                    message: 'Invalid email format',
                  },
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.email ? '2px solid red' : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.email && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.email.message}
                </span>
              )}
            </div>

            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Phone *
              </label>
              <input
                {...register('phone', {
                  required: 'Phone is required',
                  pattern: {
                    value: /^[0-9]{10,}$/,
                    message: 'Min 10 digits',
                  },
                })}
                placeholder='0123456789'
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.phone ? '2px solid red' : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.phone && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.phone.message}
                </span>
              )}
            </div>
          </div>

          <div style={{ marginTop: '16px' }}>
            <label style={{ display: 'block', marginBottom: '4px' }}>
              LinkedIn Profile URL
            </label>
            <input
              {...register('linkedin', {
                pattern: {
                  value: /^https:\/\/(www\.)?linkedin\.com\/.+/,
                  message: 'Must be valid LinkedIn URL',
                },
              })}
              placeholder='https://linkedin.com/in/yourname'
              style={{
                width: '100%',
                padding: '8px',
                border: errors.linkedin ? '2px solid red' : '1px solid #ccc',
                borderRadius: '4px',
              }}
            />
            {errors.linkedin && (
              <span style={{ color: 'red', fontSize: '14px' }}>
                {errors.linkedin.message}
              </span>
            )}
          </div>
        </section>

        {/* SECTION 2: Work Experience */}
        <section
          style={{
            marginBottom: '32px',
            padding: '20px',
            backgroundColor: '#f9f9f9',
            borderRadius: '8px',
          }}
        >
          <h2>Current/Latest Position</h2>

          <div
            style={{
              display: 'grid',
              gridTemplateColumns: '1fr 1fr',
              gap: '16px',
            }}
          >
            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Company *
              </label>
              <input
                {...register('currentCompany', {
                  required: 'Company is required',
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.currentCompany
                    ? '2px solid red'
                    : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.currentCompany && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.currentCompany.message}
                </span>
              )}
            </div>

            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Position *
              </label>
              <input
                {...register('currentPosition', {
                  required: 'Position is required',
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.currentPosition
                    ? '2px solid red'
                    : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.currentPosition && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.currentPosition.message}
                </span>
              )}
            </div>

            <div>
              <label style={{ display: 'block', marginBottom: '4px' }}>
                Years of Experience *
              </label>
              <input
                type='number'
                {...register('yearsOfExperience', {
                  required: 'Required',
                  min: { value: 0, message: 'Min 0' },
                  max: { value: 50, message: 'Max 50' },
                })}
                style={{
                  width: '100%',
                  padding: '8px',
                  border: errors.yearsOfExperience
                    ? '2px solid red'
                    : '1px solid #ccc',
                  borderRadius: '4px',
                }}
              />
              {errors.yearsOfExperience && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.yearsOfExperience.message}
                </span>
              )}
            </div>
          </div>
        </section>

        {/* SECTION 3: Skills & Preferences */}
        <section
          style={{
            marginBottom: '32px',
            padding: '20px',
            backgroundColor: '#f9f9f9',
            borderRadius: '8px',
          }}
        >
          <h2>Skills & Preferences</h2>

          <div style={{ marginBottom: '16px' }}>
            <label
              style={{
                display: 'block',
                marginBottom: '8px',
                fontWeight: 'bold',
              }}
            >
              Technical Skills * (Select at least 1)
            </label>
            <div
              style={{
                display: 'grid',
                gridTemplateColumns: '1fr 1fr',
                gap: '8px',
              }}
            >
              {['React', 'Vue', 'Angular', 'Node.js', 'Python', 'Java'].map(
                (skill) => (
                  <label
                    key={skill}
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: '8px',
                    }}
                  >
                    <input
                      type='checkbox'
                      value={skill}
                      {...register('skills', {
                        validate: (value) =>
                          (value && value.length > 0) ||
                          'Select at least 1 skill',
                      })}
                    />
                    <span>{skill}</span>
                  </label>
                ),
              )}
            </div>
            {errors.skills && (
              <span
                style={{
                  color: 'red',
                  fontSize: '14px',
                  display: 'block',
                  marginTop: '4px',
                }}
              >
                {errors.skills.message}
              </span>
            )}
          </div>

          <div style={{ marginBottom: '16px' }}>
            <label style={{ display: 'block', marginBottom: '4px' }}>
              Why do you want to join? * (Min 50 characters)
            </label>
            <textarea
              {...register('motivation', {
                required: 'This field is required',
                minLength: {
                  value: 50,
                  message: 'Please write at least 50 characters',
                },
              })}
              rows={4}
              style={{
                width: '100%',
                padding: '8px',
                border: errors.motivation ? '2px solid red' : '1px solid #ccc',
                borderRadius: '4px',
                fontFamily: 'inherit',
                resize: 'vertical',
              }}
            />
            <div
              style={{
                display: 'flex',
                justifyContent: 'space-between',
                marginTop: '4px',
              }}
            >
              {errors.motivation && (
                <span style={{ color: 'red', fontSize: '14px' }}>
                  {errors.motivation.message}
                </span>
              )}
              <span style={{ fontSize: '14px', color: '#666' }}>
                {watch('motivation')?.length || 0} / 50 characters
              </span>
            </div>
          </div>

          <div>
            <label style={{ display: 'block', marginBottom: '4px' }}>
              Expected Salary (USD/year) *
            </label>
            <input
              type='number'
              {...register('expectedSalary', {
                required: 'Expected salary is required',
                min: { value: 1000, message: 'Min $1,000' },
                max: { value: 500000, message: 'Max $500,000' },
              })}
              placeholder='50000'
              style={{
                width: '100%',
                padding: '8px',
                border: errors.expectedSalary
                  ? '2px solid red'
                  : '1px solid #ccc',
                borderRadius: '4px',
              }}
            />
            {errors.expectedSalary && (
              <span style={{ color: 'red', fontSize: '14px' }}>
                {errors.expectedSalary.message}
              </span>
            )}
          </div>
        </section>

        {/* Submit Button */}
        <button
          type='submit'
          style={{
            width: '100%',
            padding: '16px',
            backgroundColor: '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '18px',
            fontWeight: 'bold',
            cursor: 'pointer',
          }}
        >
          Submit Application
        </button>
      </form>

      {/* Testing Checklist */}
      <div
        style={{
          marginTop: '40px',
          padding: '20px',
          backgroundColor: '#f5f5f5',
          borderRadius: '8px',
        }}
      >
        <h3>🧪 Manual Testing Checklist:</h3>
        <ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
          <li>✓ Submit empty form → See all errors</li>
          <li>✓ Fill invalid email → See error on blur</li>
          <li>✓ Fill valid data → Errors disappear</li>
          <li>✓ Refresh page → Data persists (localStorage)</li>
          <li>✓ Submit valid form → Success + clear draft</li>
          <li>✓ Clear draft button → Form resets</li>
          <li>✓ Type in text area → Character counter updates</li>
          <li>✓ Select no skills → Error on submit</li>
        </ul>
      </div>
    </div>
  );
}

function getDefaultValues() {
  return {
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    linkedin: '',
    currentCompany: '',
    currentPosition: '',
    yearsOfExperience: '',
    skills: [],
    motivation: '',
    expectedSalary: '',
  };
}

/*
Architecture Decisions Summary:

1. ✅ Single-page form
   - Better overview
   - Easier review
   - No router complexity

2. ✅ onTouched validation mode
   - Validate after first blur
   - Show errors in real-time after
   - Good UX balance

3. ✅ Error summary at top
   - Overview of all errors
   - Scroll to top on submit error
   - Clear visibility

4. ✅ Auto-save to localStorage
   - Debounced (500ms)
   - Prevent data loss
   - Simple implementation

Trade-offs accepted:
- Long form (but organized in sections)
- No progress bar (but clear sections)
- localStorage limit (acceptable for form data)
*/

⭐⭐⭐⭐⭐ Bài 5: Production-Ready Survey Form (90 phút)

🎯 Mục tiêu: Code sẵn sàng ship to production

jsx
/**
 * 🎯 Mục tiêu: Production-ready Survey Form
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Customer Satisfaction Survey với:
 * - Multiple question types (text, radio, checkbox, rating, textarea)
 * - Conditional questions (show based on previous answers)
 * - Progress tracking
 * - Save & continue later
 * - Export results
 *
 * 🏗️ Technical Design Doc:
 * 1. Component Architecture:
 *    - SurveyForm (container)
 *    - QuestionRenderer (dynamic)
 *    - ProgressBar
 *    - ResultsSummary
 *
 * 2. State Management Strategy:
 *    - React Hook Form for form state
 *    - localStorage for persistence
 *    - Context for survey config (if needed)
 *
 * 3. Validation Strategy:
 *    - Required fields
 *    - Conditional validation
 *    - Min/max constraints
 *
 * 4. Performance Considerations:
 *    - Memoize expensive components
 *    - Debounce auto-save
 *    - Optimize re-renders
 *
 * 5. Error Handling Strategy:
 *    - Validation errors
 *    - localStorage errors
 *    - Edge cases
 *
 * ✅ Production Checklist:
 * - [ ] All fields validated properly
 * - [ ] Error states handled
 * - [ ] Loading states (if async)
 * - [ ] Empty states
 * - [ ] A11y: keyboard navigation
 * - [ ] A11y: ARIA labels
 * - [ ] A11y: focus management
 * - [ ] Mobile responsive (basic)
 * - [ ] Auto-save works
 * - [ ] Export functionality
 * - [ ] Clear/reset functionality
 * - [ ] Progress indicator
 * - [ ] Conditional logic works
 */
💡 Solution
jsx
/**
 * Production-Ready Customer Satisfaction Survey
 *
 * Features:
 * - Multiple question types
 * - Conditional questions
 * - Progress tracking
 * - Auto-save & restore
 * - Export results
 * - Full accessibility
 * - Error handling
 */

import { useForm } from 'react-hook-form';
import { useState, useEffect, useMemo, useCallback } from 'react';

const SURVEY_STORAGE_KEY = 'customer_survey_draft';

// Survey configuration
const SURVEY_CONFIG = {
  title: 'Customer Satisfaction Survey',
  description: 'Help us improve our service by answering a few questions',
  questions: [
    {
      id: 'satisfaction',
      type: 'radio',
      label: 'How satisfied are you with our service?',
      required: true,
      options: [
        'Very Satisfied',
        'Satisfied',
        'Neutral',
        'Dissatisfied',
        'Very Dissatisfied',
      ],
    },
    {
      id: 'recommendation',
      type: 'rating',
      label: 'How likely are you to recommend us? (0-10)',
      required: true,
      min: 0,
      max: 10,
    },
    {
      id: 'followup',
      type: 'radio',
      label: 'Would you like us to follow up with you?',
      required: true,
      options: ['Yes', 'No'],
      // Conditional: Show next question only if Yes
    },
    {
      id: 'contactMethod',
      type: 'radio',
      label: 'Preferred contact method',
      required: true,
      options: ['Email', 'Phone', 'SMS'],
      // Show only if followup === 'Yes'
      condition: (formValues) => formValues.followup === 'Yes',
    },
    {
      id: 'features',
      type: 'checkbox',
      label: 'Which features do you use? (Select all that apply)',
      required: true,
      options: ['Dashboard', 'Reports', 'API', 'Mobile App', 'Integrations'],
    },
    {
      id: 'improvements',
      type: 'textarea',
      label: 'What could we improve?',
      required: true,
      minLength: 20,
      placeholder: 'Please provide detailed feedback (min 20 characters)...',
    },
    {
      id: 'additionalComments',
      type: 'textarea',
      label: 'Any additional comments?',
      required: false,
      placeholder: 'Optional',
    },
  ],
};

function CustomerSurvey() {
  const [completedSurvey, setCompletedSurvey] = useState(null);
  const [isSaving, setIsSaving] = useState(false);

  // Load saved draft
  const loadDraft = useCallback(() => {
    try {
      const draft = localStorage.getItem(SURVEY_STORAGE_KEY);
      return draft ? JSON.parse(draft) : {};
    } catch (error) {
      console.error('Error loading draft:', error);
      return {};
    }
  }, []);

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    watch,
    reset,
  } = useForm({
    mode: 'onTouched',
    defaultValues: loadDraft(),
  });

  const formValues = watch();

  // Auto-save to localStorage (debounced)
  useEffect(() => {
    setIsSaving(true);
    const timeoutId = setTimeout(() => {
      try {
        localStorage.setItem(SURVEY_STORAGE_KEY, JSON.stringify(formValues));
        setIsSaving(false);
      } catch (error) {
        console.error('Error saving draft:', error);
        setIsSaving(false);
      }
    }, 1000);

    return () => clearTimeout(timeoutId);
  }, [formValues]);

  // Calculate progress
  const progress = useMemo(() => {
    const totalQuestions = SURVEY_CONFIG.questions.filter(
      (q) => !q.condition || q.condition(formValues),
    ).length;

    const answeredQuestions = SURVEY_CONFIG.questions.filter((q) => {
      if (q.condition && !q.condition(formValues)) return false;

      const value = formValues[q.id];
      if (Array.isArray(value)) return value.length > 0;
      return value !== undefined && value !== '';
    }).length;

    return Math.round((answeredQuestions / totalQuestions) * 100);
  }, [formValues]);

  const onSubmit = async (data) => {
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1500));

    console.log('Survey submitted:', data);
    setCompletedSurvey(data);

    // Clear draft
    localStorage.removeItem(SURVEY_STORAGE_KEY);
  };

  const clearDraft = useCallback(() => {
    if (window.confirm('Clear all answers and start over?')) {
      localStorage.removeItem(SURVEY_STORAGE_KEY);
      reset({});
      setCompletedSurvey(null);
    }
  }, [reset]);

  const exportResults = useCallback(() => {
    if (!completedSurvey) return;

    const dataStr = JSON.stringify(completedSurvey, 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_results_${Date.now()}.json`;
    link.click();
    URL.revokeObjectURL(url);
  }, [completedSurvey]);

  // If survey completed, show results
  if (completedSurvey) {
    return (
      <SurveyResults
        data={completedSurvey}
        onExport={exportResults}
        onStartNew={() => {
          setCompletedSurvey(null);
          reset({});
        }}
      />
    );
  }

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      {/* Header */}
      <header style={{ marginBottom: '32px' }}>
        <h1>{SURVEY_CONFIG.title}</h1>
        <p style={{ color: '#666' }}>{SURVEY_CONFIG.description}</p>

        {/* Progress Bar */}
        <div style={{ marginTop: '16px' }}>
          <div
            style={{
              display: 'flex',
              justifyContent: 'space-between',
              marginBottom: '4px',
            }}
          >
            <span style={{ fontSize: '14px', fontWeight: 'bold' }}>
              Progress: {progress}%
            </span>
            <span style={{ fontSize: '14px', color: '#666' }}>
              {isSaving ? '💾 Saving...' : '✓ Saved'}
            </span>
          </div>
          <div
            style={{
              width: '100%',
              height: '8px',
              backgroundColor: '#e0e0e0',
              borderRadius: '4px',
              overflow: 'hidden',
            }}
          >
            <div
              style={{
                width: `${progress}%`,
                height: '100%',
                backgroundColor: progress === 100 ? '#4caf50' : '#2196f3',
                transition: 'width 0.3s ease',
              }}
            />
          </div>
        </div>
      </header>

      {/* Form */}
      <form onSubmit={handleSubmit(onSubmit)}>
        {SURVEY_CONFIG.questions.map((question, index) => {
          // Check condition
          if (question.condition && !question.condition(formValues)) {
            return null;
          }

          return (
            <QuestionField
              key={question.id}
              question={question}
              index={index}
              register={register}
              errors={errors}
              watch={watch}
            />
          );
        })}

        {/* Actions */}
        <div
          style={{
            marginTop: '32px',
            display: 'flex',
            gap: '12px',
            flexWrap: 'wrap',
          }}
        >
          <button
            type='submit'
            disabled={isSubmitting || progress < 100}
            style={{
              flex: 1,
              minWidth: '200px',
              padding: '16px',
              backgroundColor:
                isSubmitting || progress < 100 ? '#ccc' : '#4caf50',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              fontSize: '16px',
              fontWeight: 'bold',
              cursor:
                isSubmitting || progress < 100 ? 'not-allowed' : 'pointer',
            }}
          >
            {isSubmitting ? 'Submitting...' : 'Submit Survey'}
          </button>

          <button
            type='button'
            onClick={clearDraft}
            style={{
              padding: '16px 24px',
              backgroundColor: '#f44336',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              fontSize: '16px',
              cursor: 'pointer',
            }}
          >
            Clear All
          </button>
        </div>
      </form>

      {/* A11y Info */}
      <div
        style={{
          marginTop: '40px',
          padding: '16px',
          backgroundColor: '#e3f2fd',
          borderRadius: '8px',
          fontSize: '14px',
        }}
      >
        <strong>♿ Accessibility:</strong>
        <ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
          <li>Use Tab to navigate between fields</li>
          <li>Use Space/Enter to select radio/checkbox</li>
          <li>All fields have proper labels</li>
          <li>Errors announced to screen readers</li>
        </ul>
      </div>
    </div>
  );
}

// Question Field Component
const QuestionField = ({ question, index, register, errors, watch }) => {
  const fieldStyles = {
    container: {
      marginBottom: '32px',
      padding: '20px',
      backgroundColor: '#f9f9f9',
      borderRadius: '8px',
      border: errors[question.id] ? '2px solid #f44336' : '1px solid #e0e0e0',
    },
    label: {
      display: 'block',
      marginBottom: '12px',
      fontSize: '16px',
      fontWeight: 'bold',
    },
    error: {
      display: 'block',
      color: '#f44336',
      fontSize: '14px',
      marginTop: '4px',
    },
  };

  const renderField = () => {
    switch (question.type) {
      case 'radio':
        return (
          <div
            role='radiogroup'
            aria-labelledby={`question-${question.id}`}
          >
            {question.options.map((option) => (
              <label
                key={option}
                style={{
                  display: 'block',
                  padding: '12px',
                  marginBottom: '8px',
                  backgroundColor: 'white',
                  border: '1px solid #ccc',
                  borderRadius: '4px',
                  cursor: 'pointer',
                }}
              >
                <input
                  type='radio'
                  value={option}
                  {...register(question.id, {
                    required:
                      question.required && `${question.label} is required`,
                  })}
                  style={{ marginRight: '8px' }}
                />
                {option}
              </label>
            ))}
          </div>
        );

      case 'checkbox':
        return (
          <div
            role='group'
            aria-labelledby={`question-${question.id}`}
          >
            {question.options.map((option) => (
              <label
                key={option}
                style={{
                  display: 'block',
                  padding: '12px',
                  marginBottom: '8px',
                  backgroundColor: 'white',
                  border: '1px solid #ccc',
                  borderRadius: '4px',
                  cursor: 'pointer',
                }}
              >
                <input
                  type='checkbox'
                  value={option}
                  {...register(question.id, {
                    validate: question.required
                      ? (value) =>
                          (value && value.length > 0) ||
                          'Select at least one option'
                      : undefined,
                  })}
                  style={{ marginRight: '8px' }}
                />
                {option}
              </label>
            ))}
          </div>
        );

      case 'rating':
        return (
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
            {Array.from({ length: question.max + 1 }, (_, i) => i).map(
              (num) => (
                <label
                  key={num}
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    width: '40px',
                    height: '40px',
                    backgroundColor:
                      watch(question.id) == num ? '#2196f3' : 'white',
                    color: watch(question.id) == num ? 'white' : 'black',
                    border: '2px solid #2196f3',
                    borderRadius: '4px',
                    cursor: 'pointer',
                    fontWeight: 'bold',
                  }}
                >
                  <input
                    type='radio'
                    value={num}
                    {...register(question.id, {
                      required: question.required && 'Please select a rating',
                    })}
                    style={{ display: 'none' }}
                  />
                  {num}
                </label>
              ),
            )}
          </div>
        );

      case 'textarea':
        const currentLength = watch(question.id)?.length || 0;
        return (
          <div>
            <textarea
              {...register(question.id, {
                required: question.required && `${question.label} is required`,
                minLength: question.minLength && {
                  value: question.minLength,
                  message: `Minimum ${question.minLength} characters required`,
                },
              })}
              placeholder={question.placeholder}
              rows={4}
              style={{
                width: '100%',
                padding: '12px',
                border: '1px solid #ccc',
                borderRadius: '4px',
                fontFamily: 'inherit',
                fontSize: '14px',
                resize: 'vertical',
              }}
              aria-describedby={
                question.minLength ? `${question.id}-hint` : undefined
              }
            />
            {question.minLength && (
              <div
                id={`${question.id}-hint`}
                style={{
                  marginTop: '4px',
                  fontSize: '14px',
                  color:
                    currentLength < question.minLength ? '#f44336' : '#666',
                }}
              >
                {currentLength} / {question.minLength} characters
              </div>
            )}
          </div>
        );

      default:
        return (
          <input
            type='text'
            {...register(question.id, {
              required: question.required && `${question.label} is required`,
            })}
            style={{
              width: '100%',
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '4px',
              fontSize: '14px',
            }}
          />
        );
    }
  };

  return (
    <div style={fieldStyles.container}>
      <label
        id={`question-${question.id}`}
        style={fieldStyles.label}
      >
        {index + 1}. {question.label}
        {question.required && <span style={{ color: '#f44336' }}> *</span>}
      </label>

      {renderField()}

      {errors[question.id] && (
        <span
          style={fieldStyles.error}
          role='alert'
          aria-live='polite'
        >
          {errors[question.id].message}
        </span>
      )}
    </div>
  );
};

// Results Summary Component
function SurveyResults({ data, onExport, onStartNew }) {
  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <div
        style={{
          padding: '24px',
          backgroundColor: '#e8f5e9',
          border: '2px solid #4caf50',
          borderRadius: '8px',
          marginBottom: '24px',
          textAlign: 'center',
        }}
      >
        <h1 style={{ color: '#2e7d32', margin: '0 0 8px 0' }}>
          ✅ Survey Completed!
        </h1>
        <p style={{ margin: 0, color: '#666' }}>Thank you for your feedback</p>
      </div>

      <div
        style={{
          padding: '24px',
          backgroundColor: '#f9f9f9',
          borderRadius: '8px',
          marginBottom: '24px',
        }}
      >
        <h2>Your Responses:</h2>
        <pre
          style={{
            backgroundColor: '#fff',
            padding: '16px',
            borderRadius: '4px',
            overflow: 'auto',
            fontSize: '14px',
            border: '1px solid #e0e0e0',
          }}
        >
          {JSON.stringify(data, null, 2)}
        </pre>
      </div>

      <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
        <button
          onClick={onExport}
          style={{
            flex: 1,
            minWidth: '200px',
            padding: '16px',
            backgroundColor: '#2196f3',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: 'pointer',
          }}
        >
          📥 Export Results (JSON)
        </button>

        <button
          onClick={onStartNew}
          style={{
            padding: '16px 24px',
            backgroundColor: '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: 'pointer',
          }}
        >
          Start New Survey
        </button>
      </div>
    </div>
  );
}

/*
Production Checklist:

✅ Validation
  - All required fields validated
  - Min/max constraints
  - Conditional validation
  - Custom validation rules

✅ Error Handling
  - Form validation errors
  - localStorage errors (try-catch)
  - Edge cases handled

✅ Loading/Empty States
  - Submitting state
  - Saving indicator
  - Completed state
  - Empty form state

✅ Accessibility
  - Keyboard navigation (Tab, Space, Enter)
  - ARIA labels (role, aria-labelledby, aria-describedby)
  - Focus management
  - Error announcements (role="alert", aria-live="polite")
  - Semantic HTML

✅ Performance
  - useMemo for progress calculation
  - useCallback for handlers
  - Debounced auto-save
  - Optimized re-renders

✅ Features
  - Auto-save to localStorage
  - Progress tracking
  - Conditional questions
  - Export functionality
  - Clear/reset
  - Multiple question types

✅ UX
  - Visual progress bar
  - Save indicator
  - Character counter
  - Clear error messages
  - Success state

✅ Mobile Responsive
  - Flexible layouts (flex, grid)
  - Readable font sizes
  - Touch-friendly buttons
  - Wrapping elements
*/

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

Bảng So Sánh: Manual Forms vs React Hook Form

AspectManual (useState)React Hook FormWinner
Boilerplate CodeCao (mỗi field = 3 states)Thấp (1 hook cho tất cả)✅ RHF
Performance❌ Re-render mỗi keystroke✅ Minimal re-renders✅ RHF
Bundle Size0KB (built-in)~8KBManual
ValidationPhải tự viếtBuilt-in rules✅ RHF
Learning CurveDễ hiểuCần học APIManual
TypeScript SupportPhải tự typeFull type safety✅ RHF
ScalabilityKhó với large formsDễ dàng scale✅ RHF
Form ArraysPhức tạpuseFieldArray built-in✅ RHF
Async ValidationPhải tự handleBuilt-in support✅ RHF
IntegrationN/ADễ với Zod/Yup✅ RHF

Khi nào dùng Manual Form?

Use Manual khi:

  • Form rất đơn giản (1-2 fields)
  • Không cần validation phức tạp
  • Không muốn thêm dependency
  • Learning project (hiểu React basics)

Ví dụ:

jsx
// Simple search box
const [query, setQuery] = useState('');
<input
  value={query}
  onChange={(e) => setQuery(e.target.value)}
/>;

Khi nào dùng React Hook Form?

Use RHF khi:

  • Form trung bình đến lớn (3+ fields)
  • Cần validation phức tạp
  • Performance quan trọng
  • Production code
  • Cần form arrays
  • Integration với schema validation

Ví dụ:

  • Registration forms
  • Survey forms
  • Checkout forms
  • Profile settings
  • Job applications

Decision Tree

START: Cần tạo form

  Simple form (1-2 fields)?
    ├─ YES → Manual (useState)
    └─ NO ↓

  Cần validation phức tạp?
    ├─ YES → React Hook Form
    └─ NO ↓

  Performance critical?
    ├─ YES → React Hook Form
    └─ NO ↓

  Form có arrays/dynamic fields?
    ├─ YES → React Hook Form
    └─ NO ↓

  3+ fields?
    ├─ YES → React Hook Form
    └─ NO → Manual OK (but consider RHF for consistency)

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

Bug 1: Validation không hoạt động ❌

jsx
// ❌ Code bị lỗi
function BuggyForm() {
  const { register, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input
        {...register('email')}
        required // ← BUG: HTML validation, not RHF!
      />
      <button>Submit</button>
    </form>
  );
}

// ❓ Câu hỏi: Tại sao form vẫn submit khi email empty?
💡 Giải thích & Fix

Vấn đề:

  • HTML required attribute KHÔNG phải RHF validation
  • RHF cần validation rules trong register()
  • HTML validation có thể bypass (user disable)

Fix:

jsx
// ✅ Cách đúng
<input
  {...register('email', {
    required: 'Email is required', // ← RHF validation
  })}
  // KHÔNG dùng HTML required
/>;
{
  errors.email && <span>{errors.email.message}</span>;
}

Nguyên tắc:

  • Luôn validate bằng RHF, không dựa vào HTML validation
  • HTML validation = UX enhancement, không phải security

Bug 2: Form không reset sau submit ❌

jsx
// ❌ Code bị lỗi
function BuggyForm() {
  const { register, handleSubmit } = useForm({
    defaultValues: { name: '', email: '' },
  });

  const onSubmit = (data) => {
    console.log(data);
    // ← BUG: Không reset!
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      <input {...register('email')} />
      <button>Submit</button>
    </form>
  );
}

// ❓ Sau khi submit, form vẫn giữ giá trị cũ. Tại sao?
💡 Giải thích & Fix

Vấn đề:

  • RHF KHÔNG tự động reset sau submit
  • Phải gọi reset() manually
  • Hoặc config resetOptions trong handleSubmit

Fix 1: Manual reset

jsx
// ✅ Cách 1: Gọi reset() sau submit
const { register, handleSubmit, reset } = useForm({
  defaultValues: { name: '', email: '' },
});

const onSubmit = (data) => {
  console.log(data);
  reset(); // ← Reset về defaultValues
};

Fix 2: Reset về custom values

jsx
// ✅ Cách 2: Reset về values khác
reset({ name: 'John', email: '' }); // Custom values

Nguyên tắc:

  • Form state persist sau submit (by design)
  • Luôn gọi reset() nếu cần clear form

Bug 3: Watch gây infinite re-renders ❌

jsx
// ❌ Code bị lỗi
function BuggyForm() {
  const { register, watch } = useForm();

  // BUG: watch() mỗi render tạo subscription mới!
  const allValues = watch();

  useEffect(() => {
    console.log('Values changed:', allValues);
  }, [allValues]); // ← Infinite loop!

  return (
    <form>
      <input {...register('name')} />
    </form>
  );
}

// ❓ Component bị infinite re-render. Tại sao?
💡 Giải thích & Fix

Vấn đề:

  • watch() trả về object mới mỗi render
  • Object mới → allValues thay đổi reference
  • useEffect trigger → re-render → watch() tạo object mới → loop!

Fix 1: Watch specific fields

jsx
// ✅ Cách 1: Watch specific fields (recommended)
const name = watch('name');

useEffect(() => {
  console.log('Name changed:', name);
}, [name]); // Primitive value, stable comparison

Fix 2: Use callback form

jsx
// ✅ Cách 2: Callback form (no re-render)
useEffect(() => {
  const subscription = watch((value, { name, type }) => {
    console.log(value, name, type);
  });
  return () => subscription.unsubscribe();
}, [watch]);

Fix 3: Memoize if needed

jsx
// ✅ Cách 3: Memoize (last resort)
const allValues = watch();

useEffect(() => {
  // Only log when specific field changes
  console.log('Name:', allValues.name);
}, [allValues.name]); // Specific field

Nguyên tắc:

  • Watch specific fields > watch all
  • Use callback form cho subscriptions
  • Avoid watching objects in dependencies

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

Knowledge Check

  • [ ] Hiểu sự khác biệt giữa controlled vs uncontrolled components
  • [ ] Biết khi nào dùng React Hook Form vs manual useState
  • [ ] Nắm vững register(), handleSubmit(), formState
  • [ ] Hiểu built-in validation rules (required, pattern, min/max, validate)
  • [ ] Biết cách validate cross-field (password confirmation)
  • [ ] Hiểu watch() để theo dõi form values
  • [ ] Biết cách reset() form
  • [ ] Hiểu setValue() để set value programmatically
  • [ ] Nắm được validation modes (onChange, onBlur, onSubmit, onTouched)
  • [ ] Hiểu performance benefits của RHF

Code Review Checklist

Form Setup:

  • [ ] useForm với proper mode (onBlur/onTouched recommended)
  • [ ] defaultValues được định nghĩa rõ ràng
  • [ ] Destructure cần thiết từ useForm (register, handleSubmit, formState, etc.)

Field Registration:

  • [ ] Tất cả inputs đều có {...register('fieldName')}
  • [ ] Validation rules đầy đủ (required, pattern, etc.)
  • [ ] Error messages rõ ràng, user-friendly
  • [ ] Unique name cho mỗi field

Error Handling:

  • [ ] Hiển thị errors từ formState.errors
  • [ ] Errors có styling riêng (color, border)
  • [ ] Error messages có accessibility (role="alert")

Submit Handling:

  • [ ] onSubmit với handleSubmit()
  • [ ] Handle loading state (isSubmitting)
  • [ ] Reset form sau successful submit (if needed)
  • [ ] Handle submit errors properly

UX:

  • [ ] Disable submit khi invalid (if appropriate)
  • [ ] Loading indicator khi submitting
  • [ ] Success feedback sau submit
  • [ ] Clear error messages

🏠 BÀI TẬP VỀ NHÀ

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

Profile Update Form

Tạo form cập nhật profile với:

  • Fields: avatar URL, display name, bio, location, website
  • Validation: URL format cho avatar/website, max length cho bio
  • Preview avatar khi nhập URL
  • Character counter cho bio (max 200)
  • Save button disabled khi không có thay đổi (use formState.isDirty)

Nâng cao (60 phút)

Multi-Currency Calculator Form

Tạo form chuyển đổi tiền tệ với:

  • Amount input
  • From currency (select)
  • To currency (select)
  • Exchange rate (auto-fetch hoặc manual)
  • Result display
  • History (lưu 5 conversions gần nhất vào localStorage)
  • Clear history button
  • Validation: amount > 0, currencies khác nhau

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Hook Form Official Docs

  2. Controlled vs Uncontrolled Components

Đọc thêm

  1. React Hook Form - Advanced

  2. Form Validation Best Practices


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

Kiến thức nền (Cần biết trước)

  • Ngày 13: Forms với Controlled Components (useState)
  • Ngày 24: Custom Hooks (để hiểu RHF internals)
  • Ngày 32-34: Performance (để hiểu RHF benefits)

Hướng tới (Sẽ dùng sau)

  • Ngày 42: React Hook Form Advanced (useFieldArray, watch patterns)
  • Ngày 43: Schema Validation với Zod
  • Ngày 44: Multi-step Forms
  • Ngày 45: Final Project - Registration Flow

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Validation Strategy

jsx
// ❌ Tránh: Validate mọi keystroke (bad UX)
mode: 'onChange' // Too aggressive

// ✅ Tốt hơn: Validate sau blur đầu tiên
mode: 'onTouched' // Or 'onBlur'

// Production tip: Combine strategies
mode: 'onTouched', // First validation on blur
reValidateMode: 'onChange' // Then real-time after error

2. Performance Optimization

jsx
// ❌ Tránh: Watch toàn bộ form
const allValues = watch(); // Creates new object every render

// ✅ Tốt hơn: Watch specific fields
const email = watch('email');
const password = watch('password');

// ✅ Hoặc dùng callback
useEffect(() => {
  const { unsubscribe } = watch((data) => {
    // Handle change
  });
  return () => unsubscribe();
}, [watch]);

3. Error Handling

jsx
// Production pattern: Centralized error display
const onError = (errors) => {
  // Log to monitoring service
  console.error('Form errors:', errors);

  // Show toast notification
  toast.error('Please fix form errors');

  // Scroll to first error
  const firstError = Object.keys(errors)[0];
  document.querySelector(`[name="${firstError}"]`)?.focus();
};

<form onSubmit={handleSubmit(onSubmit, onError)}>

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

Junior Level:

  1. Q: Sự khác biệt giữa controlled và uncontrolled components? A: Controlled: React state control value (useState). Uncontrolled: DOM controls value (refs). RHF uses uncontrolled approach for better performance.

  2. Q: Làm sao validate form với React Hook Form? A: Dùng validation rules trong register(): required, pattern, min/max, validate function.

Mid Level: 3. Q: Tại sao React Hook Form performance tốt hơn controlled forms? A: RHF dùng refs thay vì state, không trigger re-render mỗi keystroke. Chỉ re-render khi validation errors thay đổi.

  1. Q: Làm sao handle conditional validation? A: Dùng validate function với access đến other field values qua watch().

Senior Level: 5. Q: Design form system cho large application với nhiều loại forms khác nhau? A:

  • Shared validation rules (custom hooks)
  • Reusable field components
  • Form config driven approach
  • Integration với API error handling
  • Centralized error display
  • Analytics tracking
  1. Q: Trade-offs giữa React Hook Form vs Formik? A:
    • RHF: Better performance (uncontrolled), smaller bundle, modern API
    • Formik: More mature, larger ecosystem, familiar to many devs
    • Choice depends on: team familiarity, bundle size constraints, existing codebase

War Stories

Story 1: The Registration Form Nightmare

Situation: Registration form với 15 fields, mỗi field có useState.
Problem: Type vào email → toàn bộ form re-render → lag!
Solution: Migrate sang RHF → Performance improvement 10x.
Lesson: Controlled forms không scale cho large forms.

Story 2: The Lost Data

Situation: User điền form 10 phút, case browser crash.
Problem: Tất cả data mất vì không auto-save.
Solution: Auto-save vào localStorage mỗi 5s với RHF watch().
Lesson: Always implement draft saves cho long forms.

Story 3: The Validation Hell

Situation: Password validation với 8 rules khác nhau.
Problem: 8 validation functions, error handling phức tạp.
Solution: Dùng validate object trong RHF với named rules.
Lesson: RHF's validate object làm code clean hơn nhiều.

🎯 PREVIEW NGÀY MAI

Ngày 42: React Hook Form - Advanced

Chúng ta sẽ học:

  • useFieldArray - Quản lý dynamic form arrays
  • useFormContext - Share form state across components
  • useWatch - Optimized field watching
  • Advanced validation patterns
  • Custom field components
  • Form wizard patterns
  • Error recovery strategies

Chuẩn bị:

  • Ôn lại bài hôm nay
  • Làm bài tập về nhà
  • Suy nghĩ về use cases cần dynamic fields

🎉 Chúc mừng! Bạn đã hoàn thành Ngày 41!

Personal tech knowledge base