Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
💾 Introduction to State⚛️ Using useState Hook🔄 Updating State Correctly🎯 Initial State Values🚫 Common State Mistakes📊 Multiple State Variables🔗 State & Rendering📝 Forms with State🏗️ State Structure Best Practices
Reactjs/State/State With Forms

📋 Form State at Scale — Building Complex Forms with Validation

Enterprise forms require sophisticated state management. This section covers validation, multi-step workflows, async operations, and field-level state tracking.


🎯 Advanced Form State Structure

Complex forms need more than just field values. Track validation state, touched fields, submission state, and errors in a structured way.

A complete form state includes values, validation errors, touched fields, and submission status. Structure them together to maintain consistency.

import { useState, useCallback } from 'react';

interface FormField {
  value: any;
  error?: string;
  touched: boolean;
  isDirty: boolean;
}

interface FormState {
  fields: Record<string, FormField>;
  isSubmitting: boolean;
  submitError?: string;
  submitSuccess: boolean;
}

type FieldValidator = (value: any) => string | undefined;

interface FieldConfig {
  validators: FieldValidator[];
  initialValue: any;
}

function useAdvancedForm(
  config: Record<string, FieldConfig>,
  onSubmit: (values: Record<string, any>) => Promise<void>
) {
  const [state, setState] = useState<FormState>(() => {
    const fields: Record<string, FormField> = {};
    for (const [name, fieldConfig] of Object.entries(config)) {
      fields[name] = {
        value: fieldConfig.initialValue,
        touched: false,
        isDirty: false,
      };
    }
    return {
      fields,
      isSubmitting: false,
      submitSuccess: false,
    };
  });

  const validateField = useCallback(
    (name: string, value: any) => {
      const validators = config[name]?.validators || [];
      for (const validator of validators) {
        const error = validator(value);
        if (error) return error;
      }
      return undefined;
    },
    [config]
  );

  const setFieldValue = useCallback(
    (name: string, value: any) => {
      setState((prev) => {
        const error = validateField(name, value);
        return {
          ...prev,
          fields: {
            ...prev.fields,
            [name]: {
              value,
              error,
              touched: prev.fields[name].touched,
              isDirty: true,
            },
          },
        };
      });
    },
    [validateField]
  );

  const setFieldTouched = useCallback((name: string) => {
    setState((prev) => ({
      ...prev,
      fields: {
        ...prev.fields,
        [name]: {
          ...prev.fields[name],
          touched: true,
        },
      },
    }));
  }, []);

  const handleSubmit = useCallback(
    async (e: React.FormEvent) => {
      e.preventDefault();

      // Validate all fields
      const newFields = { ...state.fields };
      const values: Record<string, any> = {};
      let hasErrors = false;

      for (const [name, field] of Object.entries(newFields)) {
        const error = validateField(name, field.value);
        newFields[name] = { ...field, error, touched: true };
        values[name] = field.value;
        if (error) hasErrors = true;
      }

      setState((prev) => ({
        ...prev,
        fields: newFields,
      }));

      if (hasErrors) return;

      setState((prev) => ({ ...prev, isSubmitting: true, submitError: undefined }));

      try {
        await onSubmit(values);
        setState((prev) => ({
          ...prev,
          isSubmitting: false,
          submitSuccess: true,
        }));
      } catch (error) {
        setState((prev) => ({
          ...prev,
          isSubmitting: false,
          submitError: error instanceof Error ? error.message : 'Submission failed',
        }));
      }
    },
    [state.fields, validateField, onSubmit]
  );

  const reset = useCallback(() => {
    const fields: Record<string, FormField> = {};
    for (const [name, fieldConfig] of Object.entries(config)) {
      fields[name] = {
        value: fieldConfig.initialValue,
        touched: false,
        isDirty: false,
      };
    }
    setState({ fields, isSubmitting: false, submitSuccess: false });
  }, [config]);

  return {
    values: Object.fromEntries(
      Object.entries(state.fields).map(([k, v]) => [k, v.value])
    ),
    errors: Object.fromEntries(
      Object.entries(state.fields).map(([k, v]) => [k, v.error])
    ),
    touched: Object.fromEntries(
      Object.entries(state.fields).map(([k, v]) => [k, v.touched])
    ),
    isDirty: Object.values(state.fields).some((f) => f.isDirty),
    isSubmitting: state.isSubmitting,
    submitError: state.submitError,
    submitSuccess: state.submitSuccess,
    setFieldValue,
    setFieldTouched,
    handleSubmit,
    reset,
  };
}

// Usage
function RegistrationForm() {
  const form = useAdvancedForm(
    {
      email: {
        initialValue: '',
        validators: [
          (value) => (!value ? 'Email required' : undefined),
          (value) => (!value.includes('@') ? 'Invalid email' : undefined),
        ],
      },
      password: {
        initialValue: '',
        validators: [
          (value) => (!value ? 'Password required' : undefined),
          (value) => (value.length < 8 ? 'Password must be 8+ characters' : undefined),
        ],
      },
      confirmPassword: {
        initialValue: '',
        validators: [
          (value) => (!value ? 'Confirm password' : undefined),
        ],
      },
    },
    async (values) => {
      await fetch('/api/register', {
        method: 'POST',
        body: JSON.stringify(values),
      });
    }
  );

  return (
    <form onSubmit={form.handleSubmit}>
      <div>
        <input
          type="email"
          value={form.values.email}
          onChange={(e) => form.setFieldValue('email', e.target.value)}
          onBlur={() => form.setFieldTouched('email')}
          placeholder="Email"
        />
        {form.touched.email && form.errors.email && (
          <p style={{ color: 'red' }}>{form.errors.email}</p>
        )}
      </div>
      <div>
        <input
          type="password"
          value={form.values.password}
          onChange={(e) => form.setFieldValue('password', e.target.value)}
          onBlur={() => form.setFieldTouched('password')}
          placeholder="Password"
        />
        {form.touched.password && form.errors.password && (
          <p style={{ color: 'red' }}>{form.errors.password}</p>
        )}
      </div>
      {form.submitError && <p style={{ color: 'red' }}>{form.submitError}</p>}
      {form.submitSuccess && <p style={{ color: 'green' }}>Success!</p>}
      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Cross-field validation

function useAdvancedFormWithDeps(
  config: Record<string, FieldConfig & { dependsOn?: string[] }>,
  onSubmit: (values: Record<string, any>) => Promise<void>
) {
  // Similar structure but validates dependent fields together
  // When password changes, also revalidate confirmPassword

  const [state, setState] = useState<FormState>({
    fields: {},
    isSubmitting: false,
    submitSuccess: false,
  });

  const validateField = useCallback(
    (name: string, value: any, allValues: Record<string, any>) => {
      const field = config[name];
      for (const validator of field.validators) {
        const error = validator(value, allValues);
        if (error) return error;
      }
      return undefined;
    },
    [config]
  );

  const setFieldValue = useCallback(
    (name: string, value: any) => {
      setState((prev) => {
        const allValues = Object.fromEntries(
          Object.entries(prev.fields).map(([k, v]) => [k, v.value])
        );
        allValues[name] = value;

        const newFields = { ...prev.fields };
        newFields[name] = {
          ...newFields[name],
          value,
          error: validateField(name, value, allValues),
          isDirty: true,
        };

        // Revalidate dependent fields
        const dependents = Object.entries(config)
          .filter(([, cfg]) => cfg.dependsOn?.includes(name))
          .map(([fieldName]) => fieldName);

        for (const depName of dependents) {
          newFields[depName] = {
            ...newFields[depName],
            error: validateField(depName, newFields[depName].value, allValues),
          };
        }

        return { ...prev, fields: newFields };
      });
    },
    [validateField, config]
  );

  return { setFieldValue };
}

</details>

💡 Multi-Step Form State

Multi-step forms require tracking which step is active and validating step-specific fields.

Use a step index and step-specific validation. Only validate the current step when moving forward.

import { useState, useCallback } from 'react';

interface Step {
  name: string;
  fields: string[];
  label: string;
}

function useMultiStepForm(
  steps: Step[],
  allConfig: Record<string, FieldConfig>,
  onSubmit: (values: Record<string, any>) => Promise<void>
) {
  const [currentStep, setCurrentStep] = useState(0);

  // Use form state for all fields
  const form = useAdvancedForm(allConfig, onSubmit);

  const currentStepConfig = steps[currentStep];
  const currentStepFields = currentStepConfig.fields;

  const canGoNext = useCallback(() => {
    for (const fieldName of currentStepFields) {
      if (form.errors[fieldName]) return false;
    }
    return true;
  }, [form.errors, currentStepFields]);

  const goNext = useCallback(() => {
    if (canGoNext() && currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1);
    }
  }, [canGoNext, currentStep, steps.length]);

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

  return {
    ...form,
    currentStep,
    currentStepLabel: currentStepConfig.label,
    currentStepFields,
    canGoNext: canGoNext(),
    isLastStep: currentStep === steps.length - 1,
    goNext,
    goPrev,
  };
}

// Usage
function MultiStepCheckout() {
  const form = useMultiStepForm(
    [
      { name: 'shipping', label: 'Shipping', fields: ['address', 'city', 'zip'] },
      { name: 'billing', label: 'Billing', fields: ['cardNumber', 'expiry', 'cvv'] },
      { name: 'review', label: 'Review', fields: [] },
    ],
    {
      address: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
      city: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
      zip: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
      cardNumber: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
      expiry: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
      cvv: { initialValue: '', validators: [(v) => (!v ? 'Required' : undefined)] },
    },
    async (values) => {
      await fetch('/api/checkout', {
        method: 'POST',
        body: JSON.stringify(values),
      });
    }
  );

  return (
    <form onSubmit={form.handleSubmit}>
      <h2>{form.currentStepLabel}</h2>

      {form.currentStepFields.map((fieldName) => (
        <div key={fieldName}>
          <input
            value={form.values[fieldName]}
            onChange={(e) => form.setFieldValue(fieldName, e.target.value)}
            onBlur={() => form.setFieldTouched(fieldName)}
            placeholder={fieldName}
          />
          {form.touched[fieldName] && form.errors[fieldName] && (
            <p style={{ color: 'red' }}>{form.errors[fieldName]}</p>
          )}
        </div>
      ))}

      <button type="button" onClick={form.goPrev} disabled={form.currentStep === 0}>
        Back
      </button>

      {form.isLastStep ? (
        <button type="submit" disabled={form.isSubmitting}>
          Submit
        </button>
      ) : (
        <button type="button" onClick={form.goNext} disabled={!form.canGoNext}>
          Next
        </button>
      )}
    </form>
  );
}

🔧 Async Validation

Validate fields asynchronously: checking username availability, email verification, etc.

Queue async validation requests and debounce them to avoid excessive API calls.

import { useState, useCallback, useRef, useEffect } from 'react';

function useAsyncValidation<T>(
  validateFn: (value: T) => Promise<string | undefined>,
  debounceMs = 500
) {
  const [isValidating, setIsValidating] = useState(false);
  const [error, setError] = useState<string | undefined>();
  const timeoutRef = useRef<NodeJS.Timeout>();

  const validate = useCallback(
    (value: T) => {
      setIsValidating(true);

      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(async () => {
        try {
          const result = await validateFn(value);
          setError(result);
        } catch (err) {
          setError('Validation error');
        } finally {
          setIsValidating(false);
        }
      }, debounceMs);
    },
    [validateFn, debounceMs]
  );

  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return { isValidating, error, validate };
}

function CheckUsernameAvailability() {
  const [username, setUsername] = useState('');
  const { isValidating, error, validate } = useAsyncValidation(
    async (value) => {
      if (!value) return 'Username required';
      const response = await fetch(`/api/check-username?username=${value}`);
      const data = await response.json();
      return data.available ? undefined : 'Username taken';
    },
    1000
  );

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setUsername(value);
    validate(value);
  };

  return (
    <div>
      <input
        value={username}
        onChange={handleChange}
        placeholder="Choose username"
      />
      {isValidating && <p>Checking...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {!error && username && <p style={{ color: 'green' }}>Available!</p>}
    </div>
  );
}

📊 Form State Patterns Comparison

┌──────────────────┬─────────────────┬──────────────────┐
│ Pattern          │ Complexity      │ Use Case         │
├──────────────────┼─────────────────┼──────────────────┤
│ Simple useState  │ Low             │ Basic forms      │
│ useReducer       │ Medium          │ Complex forms    │
│ Custom hook      │ Medium-High     │ Reusable logic   │
│ Multi-step hook  │ High            │ Wizards          │
│ Async validation │ High            │ Server checks    │
│ Form library     │ Very High       │ Enterprise       │
└──────────────────┴─────────────────┴──────────────────┘

🔑 Key Takeaways

  • ✅ Structure form state to include values, errors, touched, and submission state
  • ✅ Validate fields both on blur and on submit
  • ✅ Show errors only for touched fields
  • ✅ Track dirty state to show unsaved changes
  • ✅ Use multi-step forms for wizard-like flows
  • ✅ Validate step-specific fields before moving forward
  • ✅ Debounce async validation to reduce API calls
  • ✅ Show loading state during async validation
  • ✅ Handle cross-field validation (dependent fields)
  • ✅ Support field-level reset without full form reset

Ready to practice? Challenges | Next: State vs Props vs Context


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn