I've been trying to implement tanstack from shadcn but had issues with displaying errors returned from failed validation by laravel so ended up with useForm from Inertia and zod for client-side. Works like a charm. Here you are:
import * as z from 'zod';
rules:
...
const LessonSchema = z.object({
title: z
.string()
.min(5, 'Title must be at least 5 characters.')
.max(10, 'Title must be at most 32 characters.')
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
});
later in the form component closure
// Local state for Zod validation errors (client-side only)
const [clientErrors, setClientErrors] = useState<ClientErrors>({});
// Function to perform client-side validation using Zod
const validateData = (formData: LessonFormData): boolean => {
setClientErrors({});
const result = LessonSchema.safeParse(formData);
if (!result.success) {
const newErrors: ClientErrors = {};
// Map Zod errors to our simple ClientErrors structure
result.error.issues.forEach((issue) => {
const path = issue.path[0] as keyof LessonFormData;
if (path) {
newErrors[path] = issue.message;
}
});
setClientErrors(newErrors);
return false; // Validation failed
}
return true; // Validation passed
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
clearErrors();
// client-side validation
if (!validateData(data)) {
// Validation failed. Stop submission and display clientErrors.
toast.error('Please fix the errors in the form.');
return;
}
post(storeRoute, {
onSuccess: () => {
toast.success('Lesson created successfully!');
// Reset form on successful submission
reset();
},
// Inertia's onError handles Laravel's validation errors (backend)
onError: (serverErrors) => {
console.error('Backend validation failed:', serverErrors);
toast.error('A server validation error occurred.');
},
});
};
// Combine client and server errors for display. Client errors take precedence.
const getError = (fieldName: keyof LessonFormData): string | undefined => {
return clientErrors[fieldName] || errors[fieldName];
};
const isError = (fieldName: keyof LessonFormData): boolean => {
return !!getError(fieldName);
};
form fields
<div>
<label htmlFor="title" className="mb-1 block text-sm font-medium text-gray-700">
Title
</label>
<input
type="text"
id="title"
value={data.title}
// Clear client-side error when user starts typing
onChange={(e) => {
setData('title', e.target.value);
if (clientErrors.title) {
setClientErrors((prev) => ({ ...prev, title: undefined }));
}
}}
className={`w-full rounded-lg border p-3 shadow-sm transition duration-150 ${
isError('title')
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-green-500'
}`}
onBlur={() => validateData(data)} // Validate on blur for immediate feedback
/>
{getError('title') && <p className="mt-1 text-sm text-red-600">{getError('title')}</p>}
</div>