📅 NGÀY 45: ⚡ Project 6 - Registration Flow
🎯 Mục tiêu học tập (5 phút)
- [ ] Tổng hợp React Hook Form + Zod + Context vào real-world project
- [ ] Implement multi-step wizard form hoàn chỉnh
- [ ] Quản lý complex form state across multiple steps
- [ ] Validate mỗi step independently với Zod schemas
- [ ] Handle file uploads trong forms
- [ ] Build production-ready registration flow
🤔 Kiểm tra đầu vào (5 phút)
- React Hook Form useForm hook có những options nào quan trọng?
- Zod schema composition hoạt động như thế nào?
- Context API pattern nào tốt nhất cho form state management?
📖 PHẦN 1: PROJECT OVERVIEW (30 phút)
1.1 Vấn Đề Thực Tế
Real-world Registration Flow Requirements:
// ❌ BAD: Single long form
function BadRegistrationForm() {
return (
<form>
{/* 20+ fields all at once */}
<input name='firstName' />
<input name='lastName' />
<input name='email' />
<input name='password' />
<input name='confirmPassword' />
<input name='phone' />
<input name='address' />
<input name='city' />
<input name='country' />
{/* ... 11 more fields */}
<button>Submit All</button>
</form>
);
}
// Problems:
// 1. User overwhelmed - "Too many fields!"
// 2. No progress indication
// 3. Validation all at once = confusing errors
// 4. Can't save partial progress
// 5. Mobile UX terrible
// 6. High abandonment rateReal-world Examples:
E-commerce Checkout:
Step 1: Shipping Address
Step 2: Payment Info
Step 3: Review Order
Step 4: Confirmation
Job Application:
Step 1: Personal Info
Step 2: Education
Step 3: Work Experience
Step 4: Documents Upload
Step 5: Review & Submit
Account Setup:
Step 1: Basic Info
Step 2: Profile Details
Step 3: Preferences
Step 4: Verification1.2 Giải Pháp: Multi-step Wizard
✅ GOOD: Progressive disclosure
// Step-by-step approach
<RegistrationWizard>
<Step1_PersonalInfo /> {/* 4-5 fields */}
<Step2_AccountDetails /> {/* 3-4 fields */}
<Step3_Preferences /> {/* 3-4 fields */}
<Step4_Review /> {/* Summary */}
</RegistrationWizard>
// Benefits:
// ✅ Less overwhelming
// ✅ Progress indication
// ✅ Validate per step
// ✅ Save partial progress
// ✅ Better mobile UX
// ✅ Higher completion rate1.3 Project Architecture
Component Structure:
RegistrationFlow/
├── RegistrationWizard.jsx // Main orchestrator
├── FormContext.jsx // Shared state
├── steps/
│ ├── Step1_PersonalInfo.jsx
│ ├── Step2_Contact.jsx
│ ├── Step3_Account.jsx
│ ├── Step4_Preferences.jsx
│ └── Step5_Review.jsx
├── components/
│ ├── ProgressBar.jsx
│ ├── StepIndicator.jsx
│ └── NavigationButtons.jsx
└── validation/
└── schemas.js // Zod schemas1.4 Technical Requirements
Must Have:
- React Hook Form for each step
- Zod validation per step
- Context for state management
- Progress indication
- Navigation (Next/Back/Submit)
- Data persistence across steps
- Review & edit capability
- Form submission
Nice to Have:
- File upload (profile picture)
- Auto-save to localStorage
- Field-level validation feedback
- Smooth transitions
- Mobile responsive
💻 PHẦN 2: PROJECT IMPLEMENTATION (120 phút)
Demo 1: Project Setup & Architecture ⭐
/**
* Registration Flow - Complete Project Setup
*
* Features:
* - Multi-step wizard (5 steps)
* - React Hook Form + Zod
* - Context for state management
* - Progress indication
* - Review & submit
*/
import { useState, createContext, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// ============= VALIDATION SCHEMAS =============
const personalInfoSchema = z.object({
firstName: z
.string()
.min(2, 'First name must be at least 2 characters')
.max(50, 'First name too long'),
lastName: z
.string()
.min(2, 'Last name must be at least 2 characters')
.max(50, 'Last name too long'),
birthDate: z.string().refine((date) => {
const age = new Date().getFullYear() - new Date(date).getFullYear();
return age >= 18;
}, 'Must be at least 18 years old'),
gender: z.enum(['male', 'female', 'other', 'prefer-not-to-say']),
});
const contactSchema = z.object({
email: z.string().email('Invalid email address').min(1, 'Email is required'),
phone: z
.string()
.regex(/^\+?[1-9]\d{9,14}$/, 'Invalid phone number')
.min(1, 'Phone is required'),
address: z.string().min(5, 'Address too short').max(200, 'Address too long'),
city: z.string().min(2, 'City required'),
country: z.string().min(2, 'Country required'),
});
const accountSchema = z
.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username too long')
.regex(
/^[a-zA-Z0-9_]+$/,
'Only letters, numbers, and underscore allowed',
),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character'),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
const preferencesSchema = z.object({
newsletter: z.boolean(),
notifications: z.enum(['all', 'important', 'none']),
language: z.enum(['en', 'vi', 'fr', 'es']),
timezone: z.string().min(1, 'Timezone required'),
});
// ============= FORM CONTEXT =============
const FormContext = createContext();
function FormProvider({ children }) {
const [formData, setFormData] = useState({
personalInfo: {},
contact: {},
account: {},
preferences: {},
});
const [currentStep, setCurrentStep] = useState(0);
const updateFormData = (step, data) => {
setFormData((prev) => ({
...prev,
[step]: { ...prev[step], ...data },
}));
};
const nextStep = () => setCurrentStep((prev) => Math.min(prev + 1, 4));
const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 0));
const goToStep = (step) => setCurrentStep(step);
return (
<FormContext.Provider
value={{
formData,
updateFormData,
currentStep,
nextStep,
prevStep,
goToStep,
}}
>
{children}
</FormContext.Provider>
);
}
function useFormContext() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within FormProvider');
}
return context;
}
// ============= PROGRESS INDICATOR =============
function ProgressBar({ currentStep, totalSteps }) {
const progress = ((currentStep + 1) / totalSteps) * 100;
return (
<div style={{ marginBottom: 32 }}>
{/* Progress bar */}
<div
style={{
width: '100%',
height: 8,
backgroundColor: '#e5e7eb',
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: `${progress}%`,
height: '100%',
backgroundColor: '#3b82f6',
transition: 'width 0.3s ease',
}}
/>
</div>
{/* Step indicators */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 16,
}}
>
{['Personal', 'Contact', 'Account', 'Preferences', 'Review'].map(
(label, idx) => (
<div
key={idx}
style={{
flex: 1,
textAlign: 'center',
fontSize: 12,
color: idx <= currentStep ? '#3b82f6' : '#9ca3af',
fontWeight: idx === currentStep ? 'bold' : 'normal',
}}
>
<div
style={{
width: 32,
height: 32,
borderRadius: '50%',
backgroundColor: idx <= currentStep ? '#3b82f6' : '#e5e7eb',
color: idx <= currentStep ? 'white' : '#6b7280',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 8px',
fontWeight: 'bold',
}}
>
{idx + 1}
</div>
{label}
</div>
),
)}
</div>
</div>
);
}
// ============= NAVIGATION BUTTONS =============
function NavigationButtons({
onBack,
onNext,
isFirstStep,
isLastStep,
isValid,
}) {
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 32,
paddingTop: 24,
borderTop: '1px solid #e5e7eb',
}}
>
<button
type='button'
onClick={onBack}
disabled={isFirstStep}
style={{
padding: '12px 24px',
backgroundColor: isFirstStep ? '#e5e7eb' : 'white',
border: '1px solid #d1d5db',
borderRadius: 8,
cursor: isFirstStep ? 'not-allowed' : 'pointer',
fontSize: 16,
fontWeight: 500,
color: isFirstStep ? '#9ca3af' : '#374151',
}}
>
← Back
</button>
<button
type='submit'
disabled={!isValid}
style={{
padding: '12px 24px',
backgroundColor: isValid ? '#3b82f6' : '#93c5fd',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: isValid ? 'pointer' : 'not-allowed',
fontSize: 16,
fontWeight: 600,
}}
>
{isLastStep ? 'Submit' : 'Next →'}
</button>
</div>
);
}
// Kết quả:
// ✅ Project structure ready
// ✅ Validation schemas defined
// ✅ Context for state management
// ✅ Progress indicator
// ✅ Navigation componentsDemo 2: Individual Steps Implementation ⭐⭐
/**
* Step Components - Each step is independent
*/
// ============= STEP 1: Personal Info =============
function Step1_PersonalInfo() {
const { formData, updateFormData, nextStep } = useFormContext();
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm({
resolver: zodResolver(personalInfoSchema),
defaultValues: formData.personalInfo,
mode: 'onChange',
});
const onSubmit = (data) => {
updateFormData('personalInfo', data);
nextStep();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2 style={{ marginBottom: 24 }}>Personal Information</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 16,
marginBottom: 16,
}}
>
<div>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
First Name *
</label>
<input
{...register('firstName')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.firstName ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.firstName && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.firstName.message}
</p>
)}
</div>
<div>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Last Name *
</label>
<input
{...register('lastName')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.lastName ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.lastName && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.lastName.message}
</p>
)}
</div>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Birth Date *
</label>
<input
type='date'
{...register('birthDate')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.birthDate ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.birthDate && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.birthDate.message}
</p>
)}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Gender *
</label>
<select
{...register('gender')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.gender ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
>
<option value=''>Select gender</option>
<option value='male'>Male</option>
<option value='female'>Female</option>
<option value='other'>Other</option>
<option value='prefer-not-to-say'>Prefer not to say</option>
</select>
{errors.gender && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.gender.message}
</p>
)}
</div>
<NavigationButtons
onNext={handleSubmit(onSubmit)}
isFirstStep={true}
isValid={isValid}
/>
</form>
);
}
// ============= STEP 2: Contact =============
function Step2_Contact() {
const { formData, updateFormData, nextStep, prevStep } = useFormContext();
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm({
resolver: zodResolver(contactSchema),
defaultValues: formData.contact,
mode: 'onChange',
});
const onSubmit = (data) => {
updateFormData('contact', data);
nextStep();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2 style={{ marginBottom: 24 }}>Contact Information</h2>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Email Address *
</label>
<input
type='email'
{...register('email')}
placeholder='you@example.com'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.email ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.email && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.email.message}
</p>
)}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Phone Number *
</label>
<input
type='tel'
{...register('phone')}
placeholder='+1234567890'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.phone ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.phone && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.phone.message}
</p>
)}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Address *
</label>
<input
{...register('address')}
placeholder='Street address'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.address ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.address && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.address.message}
</p>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
City *
</label>
<input
{...register('city')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.city ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.city && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.city.message}
</p>
)}
</div>
<div>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Country *
</label>
<input
{...register('country')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.country ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.country && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.country.message}
</p>
)}
</div>
</div>
<NavigationButtons
onBack={prevStep}
onNext={handleSubmit(onSubmit)}
isValid={isValid}
/>
</form>
);
}
// ============= STEP 3: Account =============
function Step3_Account() {
const { formData, updateFormData, nextStep, prevStep } = useFormContext();
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
} = useForm({
resolver: zodResolver(accountSchema),
defaultValues: formData.account,
mode: 'onChange',
});
const password = watch('password');
const onSubmit = (data) => {
updateFormData('account', data);
nextStep();
};
// Password strength indicator
const getPasswordStrength = (pwd) => {
if (!pwd) return { strength: 0, label: 'No password' };
let strength = 0;
if (pwd.length >= 8) strength++;
if (/[A-Z]/.test(pwd)) strength++;
if (/[a-z]/.test(pwd)) strength++;
if (/[0-9]/.test(pwd)) strength++;
if (/[^A-Za-z0-9]/.test(pwd)) strength++;
const labels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'];
return { strength, label: labels[strength - 1] || 'Very Weak' };
};
const passwordStrength = getPasswordStrength(password);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2 style={{ marginBottom: 24 }}>Account Details</h2>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Username *
</label>
<input
{...register('username')}
placeholder='Choose a username'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.username ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.username && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.username.message}
</p>
)}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Password *
</label>
<input
type='password'
{...register('password')}
placeholder='Create a strong password'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.password ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{/* Password strength indicator */}
{password && (
<div style={{ marginTop: 8 }}>
<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
{[1, 2, 3, 4, 5].map((level) => (
<div
key={level}
style={{
flex: 1,
height: 4,
backgroundColor:
level <= passwordStrength.strength
? passwordStrength.strength < 3
? '#dc2626'
: passwordStrength.strength < 4
? '#d97706'
: '#059669'
: '#e5e7eb',
borderRadius: 2,
}}
/>
))}
</div>
<p style={{ fontSize: 12, color: '#6b7280' }}>
Strength: {passwordStrength.label}
</p>
</div>
)}
{errors.password && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.password.message}
</p>
)}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Confirm Password *
</label>
<input
type='password'
{...register('confirmPassword')}
placeholder='Re-enter password'
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.confirmPassword ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
/>
{errors.confirmPassword && (
<p style={{ color: '#dc2626', fontSize: 14, marginTop: 4 }}>
{errors.confirmPassword.message}
</p>
)}
</div>
<NavigationButtons
onBack={prevStep}
onNext={handleSubmit(onSubmit)}
isValid={isValid}
/>
</form>
);
}
// ============= STEP 4: Preferences =============
function Step4_Preferences() {
const { formData, updateFormData, nextStep, prevStep } = useFormContext();
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm({
resolver: zodResolver(preferencesSchema),
defaultValues: formData.preferences.language
? formData.preferences
: {
newsletter: true,
notifications: 'important',
language: 'en',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
mode: 'onChange',
});
const onSubmit = (data) => {
updateFormData('preferences', data);
nextStep();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2 style={{ marginBottom: 24 }}>Preferences</h2>
<div style={{ marginBottom: 24 }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
cursor: 'pointer',
padding: 16,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<input
type='checkbox'
{...register('newsletter')}
style={{ width: 20, height: 20, cursor: 'pointer' }}
/>
<div>
<div style={{ fontWeight: 500 }}>Subscribe to newsletter</div>
<div style={{ fontSize: 14, color: '#6b7280' }}>
Get updates about new features and promotions
</div>
</div>
</label>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Notification Preferences *
</label>
<select
{...register('notifications')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.notifications ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
>
<option value='all'>All notifications</option>
<option value='important'>Important only</option>
<option value='none'>None</option>
</select>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Language *
</label>
<select
{...register('language')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.language ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
>
<option value='en'>English</option>
<option value='vi'>Tiếng Việt</option>
<option value='fr'>Français</option>
<option value='es'>Español</option>
</select>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Timezone *
</label>
<select
{...register('timezone')}
style={{
width: '100%',
padding: 12,
border: `1px solid ${errors.timezone ? '#dc2626' : '#d1d5db'}`,
borderRadius: 8,
fontSize: 16,
}}
>
<option value='America/New_York'>Eastern Time (ET)</option>
<option value='America/Chicago'>Central Time (CT)</option>
<option value='America/Denver'>Mountain Time (MT)</option>
<option value='America/Los_Angeles'>Pacific Time (PT)</option>
<option value='Asia/Ho_Chi_Minh'>Vietnam (ICT)</option>
<option value='Europe/London'>London (GMT)</option>
<option value='Europe/Paris'>Paris (CET)</option>
</select>
</div>
<NavigationButtons
onBack={prevStep}
onNext={handleSubmit(onSubmit)}
isValid={isValid}
/>
</form>
);
}
// Kết quả:
// ✅ Each step independent với own validation
// ✅ Data persisted in context
// ✅ Clean navigation between steps
// ✅ Real-time validation feedbackDemo 3: Review & Submit Step ⭐⭐⭐
/**
* Final Step - Review & Submit
*/
function Step5_Review() {
const { formData, prevStep, goToStep } = useFormContext();
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async () => {
setSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log('📤 Submitting registration:', formData);
setSubmitting(false);
setSubmitted(true);
};
if (submitted) {
return (
<div
style={{
textAlign: 'center',
padding: 60,
backgroundColor: '#f0fdf4',
borderRadius: 12,
border: '2px solid #86efac',
}}
>
<div style={{ fontSize: 64, marginBottom: 24 }}>✅</div>
<h2 style={{ color: '#059669', marginBottom: 16 }}>
Registration Successful!
</h2>
<p style={{ color: '#166534', fontSize: 18, marginBottom: 24 }}>
Welcome aboard, {formData.personalInfo.firstName}!
</p>
<p style={{ color: '#6b7280', fontSize: 14 }}>
A confirmation email has been sent to {formData.contact.email}
</p>
</div>
);
}
return (
<div>
<h2 style={{ marginBottom: 24 }}>Review Your Information</h2>
<p style={{ color: '#6b7280', marginBottom: 32 }}>
Please review all information before submitting. Click on any section to
edit.
</p>
{/* Personal Info Section */}
<div
style={{
padding: 20,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 16,
border: '1px solid #e5e7eb',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<h3 style={{ margin: 0 }}>Personal Information</h3>
<button
type='button'
onClick={() => goToStep(0)}
style={{
padding: '6px 12px',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
}}
>
Edit
</button>
</div>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Name
</div>
<div style={{ fontWeight: 500 }}>
{formData.personalInfo.firstName} {formData.personalInfo.lastName}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Birth Date
</div>
<div style={{ fontWeight: 500 }}>
{formData.personalInfo.birthDate}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Gender
</div>
<div style={{ fontWeight: 500, textTransform: 'capitalize' }}>
{formData.personalInfo.gender?.replace('-', ' ')}
</div>
</div>
</div>
</div>
{/* Contact Section */}
<div
style={{
padding: 20,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 16,
border: '1px solid #e5e7eb',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<h3 style={{ margin: 0 }}>Contact Information</h3>
<button
type='button'
onClick={() => goToStep(1)}
style={{
padding: '6px 12px',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
}}
>
Edit
</button>
</div>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Email
</div>
<div style={{ fontWeight: 500 }}>{formData.contact.email}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Phone
</div>
<div style={{ fontWeight: 500 }}>{formData.contact.phone}</div>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Address
</div>
<div style={{ fontWeight: 500 }}>
{formData.contact.address}, {formData.contact.city},{' '}
{formData.contact.country}
</div>
</div>
</div>
</div>
{/* Account Section */}
<div
style={{
padding: 20,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 16,
border: '1px solid #e5e7eb',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<h3 style={{ margin: 0 }}>Account Details</h3>
<button
type='button'
onClick={() => goToStep(2)}
style={{
padding: '6px 12px',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
}}
>
Edit
</button>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Username
</div>
<div style={{ fontWeight: 500 }}>{formData.account.username}</div>
</div>
</div>
{/* Preferences Section */}
<div
style={{
padding: 20,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 32,
border: '1px solid #e5e7eb',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<h3 style={{ margin: 0 }}>Preferences</h3>
<button
type='button'
onClick={() => goToStep(3)}
style={{
padding: '6px 12px',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
}}
>
Edit
</button>
</div>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Newsletter
</div>
<div style={{ fontWeight: 500 }}>
{formData.preferences.newsletter
? '✅ Subscribed'
: '❌ Not subscribed'}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Notifications
</div>
<div style={{ fontWeight: 500, textTransform: 'capitalize' }}>
{formData.preferences.notifications}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Language
</div>
<div style={{ fontWeight: 500 }}>
{formData.preferences.language.toUpperCase()}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 4 }}>
Timezone
</div>
<div style={{ fontWeight: 500 }}>
{formData.preferences.timezone}
</div>
</div>
</div>
</div>
{/* Submit Section */}
<div
style={{
padding: 24,
backgroundColor: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: 8,
marginBottom: 24,
}}
>
<div style={{ display: 'flex', alignItems: 'start', gap: 12 }}>
<input
type='checkbox'
id='terms'
defaultChecked
style={{ marginTop: 4, width: 20, height: 20 }}
/>
<label
htmlFor='terms'
style={{ fontSize: 14, color: '#1e40af' }}
>
I agree to the Terms of Service and Privacy Policy. I understand
that my information will be processed according to these terms.
</label>
</div>
</div>
<NavigationButtons
onBack={prevStep}
onNext={handleSubmit}
isLastStep={true}
isValid={!submitting}
/>
{submitting && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
}}
>
<div
style={{
padding: 40,
backgroundColor: 'white',
borderRadius: 12,
textAlign: 'center',
}}
>
<div style={{ fontSize: 48, marginBottom: 16 }}>⏳</div>
<div style={{ fontSize: 18, fontWeight: 500 }}>
Submitting your registration...
</div>
</div>
</div>
)}
</div>
);
}
// ============= MAIN WIZARD =============
function RegistrationWizard() {
const { currentStep } = useFormContext();
const steps = [
<Step1_PersonalInfo />,
<Step2_Contact />,
<Step3_Account />,
<Step4_Preferences />,
<Step5_Review />,
];
return (
<div
style={{
maxWidth: 800,
margin: '40px auto',
padding: 40,
backgroundColor: 'white',
borderRadius: 12,
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
}}
>
<h1 style={{ marginBottom: 32, textAlign: 'center' }}>
Create Your Account
</h1>
<ProgressBar
currentStep={currentStep}
totalSteps={5}
/>
{steps[currentStep]}
</div>
);
}
// ============= APP =============
function App() {
return (
<FormProvider>
<div
style={{
backgroundColor: '#f9fafb',
minHeight: '100vh',
padding: '20px 0',
}}
>
<RegistrationWizard />
</div>
</FormProvider>
);
}
// Kết quả:
// ✅ Complete multi-step wizard
// ✅ All steps validated independently
// ✅ Data persisted across steps
// ✅ Review step với edit capabilities
// ✅ Smooth submission flow
// ✅ Success confirmation🔨 PHẦN 3: ENHANCEMENTS (30 phút)
Enhancement 1: LocalStorage Persistence
💡 Solution
/**
* Auto-save to localStorage
*/
function FormProvider({ children }) {
const [formData, setFormData] = useState(() => {
// Load from localStorage on init
const saved = localStorage.getItem('registrationFormData');
return saved
? JSON.parse(saved)
: {
personalInfo: {},
contact: {},
account: {},
preferences: {},
};
});
const [currentStep, setCurrentStep] = useState(() => {
const savedStep = localStorage.getItem('registrationStep');
return savedStep ? parseInt(savedStep) : 0;
});
const updateFormData = (step, data) => {
setFormData((prev) => {
const updated = {
...prev,
[step]: { ...prev[step], ...data },
};
// Save to localStorage
localStorage.setItem('registrationFormData', JSON.stringify(updated));
return updated;
});
};
const nextStep = () => {
setCurrentStep((prev) => {
const next = Math.min(prev + 1, 4);
localStorage.setItem('registrationStep', next.toString());
return next;
});
};
const prevStep = () => {
setCurrentStep((prev) => {
const next = Math.max(prev - 1, 0);
localStorage.setItem('registrationStep', next.toString());
return next;
});
};
const clearForm = () => {
localStorage.removeItem('registrationFormData');
localStorage.removeItem('registrationStep');
setFormData({
personalInfo: {},
contact: {},
account: {},
preferences: {},
});
setCurrentStep(0);
};
return (
<FormContext.Provider
value={{
formData,
updateFormData,
currentStep,
nextStep,
prevStep,
goToStep: setCurrentStep,
clearForm,
}}
>
{children}
</FormContext.Provider>
);
}
// Kết quả:
// ✅ Form data persisted across browser refreshes
// ✅ User can continue where they left off
// ✅ Clear function to reset everythingEnhancement 2: File Upload (Profile Picture)
💡 Solution
/**
* Add profile picture upload to Step 1
*/
function Step1_PersonalInfo() {
const { formData, updateFormData, nextStep } = useFormContext();
const [preview, setPreview] = useState(
formData.personalInfo.profilePicture || null,
);
const {
register,
handleSubmit,
formState: { errors, isValid },
setValue,
} = useForm({
resolver: zodResolver(personalInfoSchema),
defaultValues: formData.personalInfo,
mode: 'onChange',
});
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
return;
}
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please upload an image file');
return;
}
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
setValue('profilePicture', reader.result);
};
reader.readAsDataURL(file);
}
};
const onSubmit = (data) => {
updateFormData('personalInfo', data);
nextStep();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2 style={{ marginBottom: 24 }}>Personal Information</h2>
{/* Profile Picture Upload */}
<div style={{ marginBottom: 24, textAlign: 'center' }}>
<div
style={{
width: 120,
height: 120,
borderRadius: '50%',
backgroundColor: '#f3f4f6',
margin: '0 auto 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
border: '3px solid #e5e7eb',
}}
>
{preview ? (
<img
src={preview}
alt='Profile'
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<span style={{ fontSize: 48, color: '#9ca3af' }}>👤</span>
)}
</div>
<label
style={{
display: 'inline-block',
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
borderRadius: 8,
cursor: 'pointer',
fontSize: 14,
}}
>
Upload Photo
<input
type='file'
accept='image/*'
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</label>
<p style={{ fontSize: 12, color: '#6b7280', marginTop: 8 }}>
Max 5MB • JPG, PNG, GIF
</p>
</div>
{/* Rest of the form... */}
</form>
);
}
// Kết quả:
// ✅ File upload với preview
// ✅ File size validation
// ✅ File type validation
// ✅ Base64 encoding for storage📊 PHẦN 4: PROJECT CHECKLIST (15 phút)
Production Checklist
Core Functionality:
- [x] Multi-step wizard (5 steps)
- [x] React Hook Form integration
- [x] Zod validation per step
- [x] Context state management
- [x] Progress indicator
- [x] Navigation (Next/Back)
- [x] Review & edit capability
- [x] Form submission
User Experience:
- [x] Real-time validation feedback
- [x] Error messages clear
- [x] Loading states during submission
- [x] Success confirmation
- [x] Smooth transitions
- [x] Mobile responsive
Data Management:
- [x] State persisted across steps
- [x] LocalStorage persistence (optional)
- [x] Form reset capability
- [x] Data validation before submit
Code Quality:
- [x] Components well-organized
- [x] Validation schemas separated
- [x] Reusable components
- [x] Clean code structure
- [x] Comments where needed
✅ PHẦN 5: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu wizard pattern và benefits
- [ ] Tôi biết cách structure multi-step forms
- [ ] Tôi biết combine React Hook Form + Zod + Context
- [ ] Tôi hiểu cách validate từng step independently
- [ ] Tôi biết persist data across steps
- [ ] Tôi biết implement navigation between steps
- [ ] Tôi biết build review & edit functionality
- [ ] Tôi hiểu form submission flow
- [ ] Tôi biết handle file uploads trong forms
- [ ] Tôi có thể build complete registration flow
Code Review Checklist
Form Structure:
- [ ] Steps logically organized
- [ ] Each step focused và không quá dài
- [ ] Clear progress indication
- [ ] Easy navigation
Validation:
- [ ] Zod schemas properly defined
- [ ] Validation per step works
- [ ] Error messages clear
- [ ] Real-time feedback
State Management:
- [ ] Context setup correctly
- [ ] State updates properly
- [ ] No unnecessary re-renders
- [ ] Data persisted correctly
User Experience:
- [ ] Smooth transitions
- [ ] Loading states
- [ ] Success feedback
- [ ] Error handling
- [ ] Mobile friendly
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Thêm validation rules:
- Async username availability check
- Password strength requirements customizable
- Email domain validation (corporate emails only)
- Phone number country code validation
Nâng cao (60 phút)
Enhance registration flow:
- Add "Save as draft" functionality
- Implement "Resume later" with email link
- Add progress auto-save every 30s
- Implement form analytics (track step completion rates)
- Add A/B testing capability for different flows
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
Đọc thêm
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền
- Ngày 41: React Hook Form Basics
- Ngày 42: React Hook Form Advanced
- Ngày 43: Zod Validation Schemas
- Ngày 44: Multi-step Forms Theory
- Ngày 36-38: Context API
Hướng tới
- Ngày 46: React 18 Features Introduction
- Ngày 47-50: Concurrent Features
- Testing: Testing multi-step forms
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. Analytics Integration:
// Track step completion
const nextStep = () => {
analytics.track('form_step_completed', {
step: currentStep,
stepName: stepNames[currentStep],
timeSpent: Date.now() - stepStartTime,
});
setCurrentStep((prev) => prev + 1);
};2. A/B Testing:
// Test different flows
const flowVariant = useABTest('registration-flow');
const steps =
flowVariant === 'A'
? [Step1, Step2, Step3, Step4, Step5] // Original
: [Step1Combined, Step2, Step3Review]; // Shorter3. Abandoned Cart Recovery:
// Email reminder for incomplete registrations
useEffect(() => {
if (currentStep > 0 && currentStep < 4) {
// User started but didn't finish
const saveProgress = async () => {
await fetch('/api/save-progress', {
method: 'POST',
body: JSON.stringify({
email: formData.contact?.email,
step: currentStep,
data: formData,
}),
});
};
const timer = setTimeout(saveProgress, 5 * 60 * 1000); // 5 min
return () => clearTimeout(timer);
}
}, [currentStep]);Câu Hỏi Phỏng Vấn
Junior:
- Multi-step form có lợi ích gì so với single-page form?
- Làm sao persist data across steps?
- Validation từng step hoạt động như thế nào?
Mid:
- So sánh approaches khác nhau cho multi-step forms
- Optimize performance trong wizard forms
- Handle errors trong multi-step submissions
Senior:
- Design system architecture cho complex form flows
- A/B testing strategies cho form optimization
- Analytics và conversion tracking trong forms
War Stories
Story: The 20-Step Monster
"Client yêu cầu onboarding form với 20 steps. Users abandoned massively (85% drop-off).
Solution:
- Reduced to 5 essential steps
- Made 10 steps optional (edit profile later)
- Removed 5 steps entirely (not needed)
- Added progress save every step
Result: Completion rate từ 15% → 65%
Lesson: Less is more. Only ask what you NEED, not what you WANT."
🎯 PREVIEW NGÀY 46
Ngày mai bắt đầu Phase 5: Modern React Features!
Chúng ta sẽ học về:
- React 18 Concurrent Rendering
- Automatic Batching
- Performance benefits
- Breaking changes to know
Chuẩn bị khám phá những tính năng mới nhất của React! 🚀
🎉 CHÚC MỪNG! Bạn đã hoàn thành Phase 4: Advanced Patterns!
Bạn đã master: ✅ Context API ✅ Component Patterns ✅ React Hook Form ✅ Zod Validation ✅ Multi-step Forms ✅ Production-ready Registration Flow
40+ ngày React journey đã hoàn thành! Bạn đã sẵn sàng cho Modern React Features! 💪