Skip to content
intermediatePhase 37 · Frontend Architecture

Form Architecture

Design scalable forms with validation, state management, and submission handling.

45m
0 problems
Topic Progress0%

Form State Management

Form State Management

Efficiently manage form state, validation, and submission.

React Hook Form

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

const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  age: z.number().min(18, 'Must be at least 18'),
  items: z.array(z.object({
    name: z.string().min(1),
    quantity: z.number().min(1),
  })),
});

function OrderForm() {
  const {
    register,
    handleSubmit,
    control,
    formState: { errors, isSubmitting, isDirty },
    watch,
    setValue,
  } = useForm({
    resolver: zodResolver(schema),
    defaultValues: {
      name: '',
      email: '',
      age: 0,
      items: [{ name: '', quantity: 1 }],
    },
  });

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

  const onSubmit = async (data) => {
    try {
      await submitOrder(data);
      showToast('Order submitted!');
    } catch (error) {
      showToast('Failed to submit order', 'error');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" {...register('name')} />
        {errors.name && <span className="error">{errors.name.message}</span>}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="email" {...register('email')} />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label htmlFor="age">Age</label>
        <input id="age" type="number" {...register('age', { valueAsNumber: true })} />
        {errors.age && <span className="error">{errors.age.message}</span>}
      </div>

      <div>
        <h3>Items</h3>
        {fields.map((field, index) => (
          <div key={field.id} className="item-row">
            <input
              {...register(`items.${index}.name`)}
              placeholder="Item name"
            />
            <input
              type="number"
              {...register(`items.${index}.quantity`, { valueAsNumber: true })}
            />
            <button type="button" onClick={() => remove(index)}>Remove</button>
          </div>
        ))}
        <button type="button" onClick={() => append({ name: '', quantity: 1 })}>
          Add Item
        </button>
      </div>

      <button type="submit" disabled={isSubmitting || !isDirty}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

Formik

import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const validationSchema = Yup.object({
  firstName: Yup.string().required('Required'),
  lastName: Yup.string().required('Required'),
  email: Yup.string().email('Invalid email').required('Required'),
});

function SignupForm() {
  return (
    <Formik
      initialValues={{ firstName: '', lastName: '', email: '' }}
      validationSchema={validationSchema}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <div>
            <label htmlFor="firstName">First Name</label>
            <Field id="firstName" name="firstName" />
            <ErrorMessage name="firstName" component="span" className="error" />
          </div>

          <div>
            <label htmlFor="lastName">Last Name</label>
            <Field id="lastName" name="lastName" />
            <ErrorMessage name="lastName" component="span" className="error" />
          </div>

          <div>
            <label htmlFor="email">Email</label>
            <Field id="email" name="email" type="email" />
            <ErrorMessage name="email" component="span" className="error" />
          </div>

          <button type="submit" disabled={isSubmitting}>
            Submit
          </button>
        </Form>
      )}
    </Formik>
  );
}

Choosing a Library

Library Best For
React Hook Form Performance, minimal re-renders
Formik Simple forms, easy to learn
Final Form Subscription-based updates
Custom hooks Full control, specific needs

Validation Libraries

Validation Libraries

Zod

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1, 'Name is required').max(100),
  email: z.string().email('Invalid email'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase letter')
    .regex(/[0-9]/, 'Must contain number'),
  age: z.number().min(18).max(120),
  role: z.enum(['user', 'admin']),
  website: z.string().url().optional(),
  tags: z.array(z.string()).min(1, 'At least one tag required'),
});

// Inferred type
type User = z.infer<typeof userSchema>;

// Validate data
const result = userSchema.safeParse(formData);
if (!result.success) {
  console.error(result.error.format());
}

Yup

import * as Yup from 'yup';

const validationSchema = Yup.object({
  username: Yup.string()
    .required('Username is required')
    .min(3, 'Must be at least 3 characters')
    .max(20, 'Must be 20 characters or less'),
  email: Yup.string()
    .email('Invalid email address')
    .required('Email is required'),
  password: Yup.string()
    .required('Password is required')
    .min(8, 'Password must be at least 8 characters')
    .matches(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/,
      'Must contain uppercase, lowercase, and number'
    ),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password')], 'Passwords must match')
    .required('Please confirm your password'),
  birthDate: Yup.date()
    .max(new Date(), 'Birth date cannot be in the future')
    .required('Birth date is required'),
});

Custom Validation

// Async validation
const checkEmailUnique = async (email) => {
  const response = await fetch(`/api/check-email?email=${email}`);
  const { exists } = await response.json();
  return !exists;
};

const schemaWithAsync = z.object({
  email: z.string().email().refine(
    async (email) => await checkEmailUnique(email),
    'Email already in use'
  ),
});

// Cross-field validation
const passwordSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  { message: 'Passwords do not match', path: ['confirmPassword'] }
);

Form Wizards

Form Wizards

Multi-step forms with progress tracking and state persistence.

Wizard Hook

// hooks/useFormWizard.js
function useFormWizard(steps) {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({});
  const [errors, setErrors] = useState({});

  const updateData = useCallback((stepData) => {
    setFormData(prev => ({ ...prev, ...stepData }));
  }, []);

  const validateCurrentStep = useCallback(async () => {
    const step = steps[currentStep];
    if (!step.validate) return true;

    try {
      await step.validate(formData);
      setErrors(prev => ({ ...prev, [currentStep]: null }));
      return true;
    } catch (error) {
      setErrors(prev => ({ ...prev, [currentStep]: error }));
      return false;
    }
  }, [currentStep, steps, formData]);

  const nextStep = useCallback(async () => {
    const isValid = await validateCurrentStep();
    if (isValid && currentStep < steps.length - 1) {
      setCurrentStep(prev => prev + 1);
    }
  }, [currentStep, steps.length, validateCurrentStep]);

  const prevStep = useCallback(() => {
    if (currentStep > 0) {
      setCurrentStep(prev => prev - 1);
    }
  }, [currentStep]);

  const goToStep = useCallback(async (step) => {
    // Validate all steps before the target
    for (let i = 0; i < step; i++) {
      const isValid = await validateCurrentStep();
      if (!isValid) return;
    }
    setCurrentStep(step);
  }, [validateCurrentStep]);

  return {
    currentStep,
    totalSteps: steps.length,
    formData,
    errors,
    updateData,
    nextStep,
    prevStep,
    goToStep,
    isFirstStep: currentStep === 0,
    isLastStep: currentStep === steps.length - 1,
    progress: ((currentStep + 1) / steps.length) * 100,
  };
}

Wizard Component

// components/FormWizard.jsx
function FormWizard({ steps, onSubmit }) {
  const wizard = useFormWizard(steps);
  const CurrentStepComponent = steps[wizard.currentStep].component;

  return (
    <div className="form-wizard">
      <div className="wizard-progress">
        <div className="progress-bar" style={{ width: `${wizard.progress}%` }} />
        <div className="step-indicators">
          {steps.map((step, index) => (
            <button
              key={index}
              className={`step-indicator ${
                index === wizard.currentStep ? 'active' : ''
              } ${index < wizard.currentStep ? 'completed' : ''}`}
              onClick={() => wizard.goToStep(index)}
              disabled={index > wizard.currentStep}
            >
              {index + 1}
            </button>
          ))}
        </div>
      </div>

      <div className="wizard-content">
        <CurrentStepComponent
          data={wizard.formData}
          errors={wizard.errors[wizard.currentStep]}
          onChange={wizard.updateData}
        />
      </div>

      <div className="wizard-actions">
        {!wizard.isFirstStep && (
          <button onClick={wizard.prevStep}>Previous</button>
        )}
        {wizard.isLastStep ? (
          <button onClick={() => onSubmit(wizard.formData)}>
            Submit
          </button>
        ) : (
          <button onClick={wizard.nextStep}>Next</button>
        )}
      </div>
    </div>
  );
}

// Usage
const steps = [
  { component: PersonalInfoStep, validate: validatePersonalInfo },
  { component: AddressStep, validate: validateAddress },
  { component: PaymentStep, validate: validatePayment },
  { component: ReviewStep },
];

<FormWizard steps={steps} onSubmit={handleSubmit} />

State Persistence

// Persist wizard state in localStorage
function usePersistedWizard(steps) {
  const wizard = useFormWizard(steps);

  useEffect(() => {
    localStorage.setItem('wizardState', JSON.stringify({
      currentStep: wizard.currentStep,
      formData: wizard.formData,
    }));
  }, [wizard.currentStep, wizard.formData]);

  useEffect(() => {
    const saved = localStorage.getItem('wizardState');
    if (saved) {
      const state = JSON.parse(saved);
      // Restore state...
    }
  }, []);

  return wizard;
}

Practice Problems

0/3solved
Build Form Architecture Component

Create a reusable React component implementing Form Architecture. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Form Architecture Testing

Write unit and integration tests for Form Architecture using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Form Architecture Performance

Optimize Form Architecture for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What is the main advantage of React Hook Form?

Question 1 options

2. Why use Zod for validation?

Question 2 options

3. What is cross-field validation?

Question 3 options

4. How should form wizards handle navigation?

Question 4 options

5. Why persist wizard state?

Question 5 options

Flashcards

Question

What is the difference between React Hook Form and Formik?

Answer

React Hook Form uses uncontrolled components for performance; Formik uses controlled components for simplicity.

Question

What is Zod?

Answer

A TypeScript-first schema validation library that provides type inference from schemas.

Question

What is useFieldArray?

Answer

A React Hook Form hook for managing dynamic array fields like add/remove items.

Question

How do form wizards handle state?

Answer

They track current step, accumulate form data, and validate each step before proceeding.

Question

What is Form Architecture?

Answer

Form Architecture is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.React Hook Form offers better performance with uncontrolled components
  • 2.Zod provides TypeScript inference and composable validation schemas
  • 3.Always validate before allowing navigation in form wizards
  • 4.Persist wizard state to prevent data loss
  • 5.Use cross-field validation for related inputs

Interview Tips

  • Explain the difference between controlled and uncontrolled components
  • Discuss form validation strategies (client vs server)
  • Know how to implement multi-step forms with state management

Cheat Sheet

Form Architecture Cheat Sheet

React Hook Form

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema)
});

Zod Schema

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

Form Wizard

  • Track current step
  • Validate before proceeding
  • Accumulate form data
  • Persist state in localStorage

Key Concepts

  • Controlled vs uncontrolled components
  • Client vs server validation
  • Async validation
  • Cross-field validation