Displaying Validation Errors in the Form
Displaying Validation Errors in the Form
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The violations array from chapter 20 has, SO FAR, ended up in the generic isError state from chapter 78 – this chapter connects it WITH the INDIVIDUAL form fields.
Defining the error type
export interface Violation {
propertyPath: string;
message: string;
}
export interface ApiErrorResponse {
violations?: Violation[];
}Extracting violations from the axios error
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { apiClient } from '../api/client';
import type { Project } from '../types/project';
import type { ApiErrorResponse, Violation } from '../types/error';
interface CreateProjectInput {
name: string;
description?: string;
}
export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreateProjectInput) => {
const response = await apiClient.post<Project>('/projects', input);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
},
});
}
export function extractViolations(error: unknown): Violation[] {
if (isAxiosError<ApiErrorResponse>(error) && error.response?.data.violations) {
return error.response.data.violations;
}
return [];
}isAxiosError from axios itself is a TYPE GUARD that PROVES to TypeScript that error has the expected response.data structure – WITHOUT this guard, TypeScript would treat error as unknown and REFUSE EVERY field access.
Extending the form with error display
import { useState, type FormEvent } from 'react';
import { useCreateProject, extractViolations } from '../hooks/useCreateProject';
function CreateProjectForm() {
const [name, setName] = useState('');
const createProject = useCreateProject();
const violations = createProject.isError
? extractViolations(createProject.error)
: [];
function fieldError(field: string): string | undefined {
return violations.find((v) => v.propertyPath === field)?.message;
}
function handleSubmit(event: FormEvent) {
event.preventDefault();
createProject.mutate({ name }, { onSuccess: () => setName('') });
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
{fieldError('name') && <p className="error">{fieldError('name')}</p>}
<button type="submit" disabled={createProject.isPending}>
Create project
</button>
</form>
);
}
export default CreateProjectForm;fieldError('name') looks up SPECIFICALLY for ONE violation with propertyPath === 'name' – EXACTLY the mechanism chapter 20 already PREVIEWED: propertyPath is what makes the FIELD-PRECISE mapping POSSIBLE in the first place.
Achtung: #[UniqueProjectName] from chapter 26 delivers ITS message ALSO with propertyPath: 'name' – the form displays CUSTOM validation constraints AND CUSTOM business rules (uniqueness) in the SAME UI element, WITHOUT the frontend having to distinguish BETWEEN the two.
Tipp: React Hook Form (DELIBERATELY NOT used in this course, to show the BASIC mechanics without an additional library) would make this EXACT fieldError mechanism EVEN more ergonomic via setError(propertyPath, { message }), WITHOUT changing the underlying principle.