Skip to main content

Validation

This guide covers validation strategies and best practices for ABP React applications, including form validation, schema validation, and server-side validation integration.

Overview

Validation is crucial for ensuring data integrity and providing a good user experience. ABP React provides multiple layers of validation:

  • Client-side validation: Immediate feedback for users
  • Schema validation: Type-safe validation with Zod
  • Server-side validation: ABP Framework validation integration
  • Real-time validation: Dynamic validation based on user input

Schema Validation with Zod

Zod is the recommended schema validation library for ABP React applications.

Basic Schema Definition

import { z } from 'zod';

const userSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
age: z.number().min(18).max(120),
role: z.enum(['admin', 'user', 'moderator']),
});

type User = z.infer<typeof userSchema>;

Complex Validation Rules

const passwordSchema = z.object({
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});

Conditional Validation

const conditionalSchema = z.object({
type: z.enum(['individual', 'company']),
name: z.string(),
companyName: z.string().optional(),
taxId: z.string().optional(),
}).refine((data) => {
if (data.type === 'company') {
return data.companyName && data.taxId;
}
return true;
}, {
message: 'Company name and tax ID are required for company accounts',
});

Form Validation Integration

React Hook Form with Zod

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

const UserForm = () => {
const form = useForm<User>({
resolver: zodResolver(userSchema),
defaultValues: {
username: '',
email: '',
age: 18,
role: 'user',
},
});

const onSubmit = (data: User) => {
// Handle form submission
};

return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('username')} />
{form.formState.errors.username && (
<span>{form.formState.errors.username.message}</span>
)}
{/* Other form fields */}
</form>
);
};

Custom Validation Hooks

import { useState, useEffect } from 'react';

export const useFieldValidation = (
value: string,
validationFn: (value: string) => Promise<boolean>,
delay: number = 500
) => {
const [isValid, setIsValid] = useState<boolean | null>(null);
const [isValidating, setIsValidating] = useState(false);

useEffect(() => {
const timer = setTimeout(async () => {
if (value) {
setIsValidating(true);
try {
const result = await validationFn(value);
setIsValid(result);
} catch (error) {
setIsValid(false);
} finally {
setIsValidating(false);
}
} else {
setIsValid(null);
}
}, delay);

return () => clearTimeout(timer);
}, [value, validationFn, delay]);

return { isValid, isValidating };
};

ABP Framework Validation Integration

Server-Side Validation

import { useApi } from '@/hooks/useApi';

const useServerValidation = () => {
const { post } = useApi();

const validateField = async (field: string, value: any) => {
try {
const response = await post('/api/validation/field', {
field,
value,
});
return response.isValid;
} catch (error) {
return false;
}
};

return { validateField };
};

ABP Validation Error Handling

import { useForm } from 'react-hook-form';
import { useApi } from '@/hooks/useApi';

const useAbpForm = <T>(schema: z.ZodSchema<T>) => {
const { post } = useApi();
const form = useForm<T>({
resolver: zodResolver(schema),
});

const submitWithAbpValidation = async (data: T) => {
try {
const response = await post('/api/your-endpoint', data);
return response;
} catch (error: any) {
// Handle ABP validation errors
if (error.validationErrors) {
error.validationErrors.forEach((validationError: any) => {
form.setError(validationError.memberNames[0] as any, {
type: 'server',
message: validationError.errorMessage,
});
});
}
throw error;
}
};

return { form, submitWithAbpValidation };
};

Real-Time Validation

Debounced Validation

import { useDebounce } from '@/hooks/useDebounce';

const useRealTimeValidation = (
value: string,
validationFn: (value: string) => Promise<boolean>
) => {
const debouncedValue = useDebounce(value, 300);
const [validationResult, setValidationResult] = useState<{
isValid: boolean;
message?: string;
} | null>(null);

useEffect(() => {
if (debouncedValue) {
validationFn(debouncedValue)
.then((isValid) => setValidationResult({ isValid }))
.catch((error) => setValidationResult({ isValid: false, message: error.message }));
}
}, [debouncedValue, validationFn]);

return validationResult;
};

Async Validation

const useAsyncValidation = () => {
const validateUsername = async (username: string) => {
const response = await fetch(`/api/validation/username?username=${username}`);
const result = await response.json();
return result.isAvailable;
};

const validateEmail = async (email: string) => {
const response = await fetch(`/api/validation/email?email=${email}`);
const result = await response.json();
return result.isAvailable;
};

return { validateUsername, validateEmail };
};

Validation Components

Validation Message Component

interface ValidationMessageProps {
error?: string;
isValid?: boolean;
isValidating?: boolean;
}

const ValidationMessage: React.FC<ValidationMessageProps> = ({
error,
isValid,
isValidating,
}) => {
if (isValidating) {
return <span className="text-blue-500">Validating...</span>;
}

if (error) {
return <span className="text-red-500">{error}</span>;
}

if (isValid) {
return <span className="text-green-500">✓ Valid</span>;
}

return null;
};

Validated Input Component

interface ValidatedInputProps {
name: string;
label: string;
validation?: (value: string) => Promise<boolean>;
register: any;
errors: any;
}

const ValidatedInput: React.FC<ValidatedInputProps> = ({
name,
label,
validation,
register,
errors,
}) => {
const [value, setValue] = useState('');
const validationResult = useRealTimeValidation(value, validation || (() => Promise.resolve(true)));

return (
<div>
<label htmlFor={name}>{label}</label>
<input
{...register(name)}
onChange={(e) => setValue(e.target.value)}
className={errors[name] ? 'border-red-500' : ''}
/>
<ValidationMessage
error={errors[name]?.message}
isValid={validationResult?.isValid}
isValidating={validationResult === null}
/>
</div>
);
};

Testing Validation

Unit Testing Schemas

import { describe, it, expect } from 'vitest';
import { userSchema } from './schemas';

describe('User Schema Validation', () => {
it('should validate a valid user', () => {
const validUser = {
username: 'john_doe',
email: 'john@example.com',
age: 25,
role: 'user',
};

const result = userSchema.safeParse(validUser);
expect(result.success).toBe(true);
});

it('should reject invalid email', () => {
const invalidUser = {
username: 'john_doe',
email: 'invalid-email',
age: 25,
role: 'user',
};

const result = userSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
expect(result.error?.issues[0].message).toContain('Invalid email');
});
});

Integration Testing

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { UserForm } from './UserForm';

describe('UserForm Validation', () => {
it('should show validation errors for invalid input', async () => {
render(<UserForm />);

const emailInput = screen.getByLabelText('Email');
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.blur(emailInput);

await waitFor(() => {
expect(screen.getByText(/Invalid email/)).toBeInTheDocument();
});
});

it('should validate in real-time', async () => {
render(<UserForm />);

const usernameInput = screen.getByLabelText('Username');
fireEvent.change(usernameInput, { target: { value: 'ab' } });

await waitFor(() => {
expect(screen.getByText(/at least 3 characters/)).toBeInTheDocument();
});
});
});

Best Practices

1. Progressive Validation

  • Start with basic client-side validation
  • Add real-time validation for critical fields
  • Implement server-side validation for security

2. User Experience

  • Provide immediate feedback
  • Use clear, actionable error messages
  • Show validation state visually
  • Debounce real-time validation

3. Performance

  • Debounce async validation calls
  • Cache validation results when appropriate
  • Use optimistic validation for better UX

4. Security

  • Never rely solely on client-side validation
  • Validate on both client and server
  • Sanitize user input
  • Use ABP Framework validation attributes

5. Accessibility

  • Associate error messages with form fields
  • Use proper ARIA attributes
  • Provide keyboard navigation support

Common Validation Patterns

Email Validation

const emailSchema = z
.string()
.email('Please enter a valid email address')
.min(1, 'Email is required');

Password Validation

const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[a-z]/, 'Must contain at least one lowercase letter')
.regex(/[0-9]/, 'Must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Must contain at least one special character');

Phone Number Validation

const phoneSchema = z
.string()
.regex(/^\+?[\d\s\-\(\)]+$/, 'Please enter a valid phone number')
.min(10, 'Phone number must be at least 10 digits');

URL Validation

const urlSchema = z
.string()
.url('Please enter a valid URL')
.refine((url) => url.startsWith('https://'), {
message: 'URL must use HTTPS',
});

Error Handling

Validation Error Types

interface ValidationError {
field: string;
message: string;
code?: string;
}

interface ValidationResult {
isValid: boolean;
errors: ValidationError[];
}

Error Boundary for Validation

class ValidationErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
console.error('Validation error:', error, errorInfo);
}

render() {
if (this.state.hasError) {
return <div>Something went wrong with validation. Please try again.</div>;
}

return this.props.children;
}
}

Conclusion

Effective validation is essential for building robust ABP React applications. By combining client-side validation with server-side validation and providing real-time feedback, you can create a smooth user experience while ensuring data integrity.

Remember to:

  • Use Zod for type-safe schema validation
  • Integrate with ABP Framework validation
  • Provide clear, actionable error messages
  • Test validation thoroughly
  • Follow accessibility best practices

For more information on specific validation scenarios, see the Forms and Custom Components documentation.