
ReactJS
Enterprise forms require sophisticated state management. This section covers validation, multi-step workflows, async operations, and field-level state tracking.
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 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>
);
}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>
);
}┌──────────────────┬─────────────────┬──────────────────┐
│ 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 │
└──────────────────┴─────────────────┴──────────────────┘Ready to practice? Challenges | Next: State vs Props vs Context
Resources
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