Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Obsługa błędów formularzy i komunikaty

Efektywna obsługa błędów formularzy to kluczowy element tworzenia przyjaznych dla użytkownika aplikacji webowych. Dobrze zaprojektowane komunikaty błędów nie tylko informują użytkownika o problemach, ale także wskazują, jak je rozwiązać. W tym module omowimy najlepsze praktyki obsługi błędów w formularzach React i Next.js, od projektowania przyjaznych komunikatów aż po zaawansowane techniki walidacji.

Rodzaje błędów w formularzach

Przed zagłębieniem się w szczegóły implementacji, warto zrozumieć różne rodzaje błędów, które mogą wystąpić w formularzach:

  1. Błędy walidacji - związane z nieprawidłowymi danymi wprowadzonymi przez użytkownika (np. niepoprawny format email)
  2. Błędy serwera - problemy podczas przetwarzania danych po stronie serwera (np. błąd bazy danych)
  3. Błędy sieci - problemy z połączeniem (np. timeout, offline)
  4. Błędy biznesowe - dane są technicznie poprawne, ale naruszają reguły biznesowe (np. duplikat adresu email)
  5. Błędy dostępności - formularz jest trudny lub niemożliwy do użycia dla osób z niepełnosprawnościami

Projektowanie przyjaznych komunikatów błędów

Dobrze zaprojektowane komunikaty błędów powinny spełniać następujące kryteria:

1. Być jasne i konkretne

Źle: "Błąd walidacji" Dobrze: "Hasło musi zawierać co najmniej 8 znaków, w tym cyfrę i wielką literę"

2. Używać języka przyjaznego dla użytkownika

Źle: "Wystąpił błąd parsowania JSON: nieoczekiwany token na pozycji 42" Dobrze: "Wystąpił problem techniczny. Spróbuj ponownie lub skontaktuj się z nami."

3. Sugerować rozwiązanie

Źle: "Niepoprawny format numeru telefonu" Dobrze: "Wprowadź numer telefonu w formacie 123-456-789"

4. Być widoczne w odpowiednim miejscu

Komunikaty błędów powinny być wyświetlane blisko problematycznego pola, a nie tylko na górze formularza.

5. Mieć odpowiedni kontrast i dostępność

Komunikaty błędów powinny być dobrze widoczne i dostępne dla wszystkich użytkowników, w tym osób korzystających z czytników ekranu.

Implementacja obsługi błędów w formularzach React

Przejdźmy do praktycznych przykładów implementacji obsługi błędów w różnych podejściach do formularzy.

Natywne formularze React

W natywnych formularzach React, błędy możemy obsługiwać za pomocą stanu:

1import { useState, FormEvent, ChangeEvent } from 'react';
2
3type FormErrors = {
4  name?: string;
5  email?: string;
6  password?: string;
7  generalError?: string;
8};
9
10export default function SignupForm() {
11  const [formData, setFormData] = useState({
12    name: '',
13    email: '',
14    password: ''
15  });
16  
17  const [errors, setErrors] = useState<FormErrors>({});
18  const [isSubmitting, setIsSubmitting] = useState(false);
19  const [submitSuccess, setSubmitSuccess] = useState(false);
20  
21  const validateForm = (): boolean => {
22    const newErrors: FormErrors = {};
23    let isValid = true;
24    
25    // Walidacja imienia
26    if (!formData.name.trim()) {
27      newErrors.name = 'Imię jest wymagane';
28      isValid = false;
29    }
30    
31    // Walidacja email
32    if (!formData.email.trim()) {
33      newErrors.email = 'Email jest wymagany';
34      isValid = false;
35    } else if (!/^S+@S+.S+$/.test(formData.email)) {
36      newErrors.email = 'Podaj prawidłowy adres email';
37      isValid = false;
38    }
39    
40    // Walidacja hasła
41    if (!formData.password) {
42      newErrors.password = 'Hasło jest wymagane';
43      isValid = false;
44    } else if (formData.password.length < 8) {
45      newErrors.password = 'Hasło musi mieć co najmniej 8 znaków';
46      isValid = false;
47    } else if (!/[A-Z]/.test(formData.password)) {
48      newErrors.password = 'Hasło musi zawierać co najmniej jedną wielką literę';
49      isValid = false;
50    } else if (!/[0-9]/.test(formData.password)) {
51      newErrors.password = 'Hasło musi zawierać co najmniej jedną cyfrę';
52      isValid = false;
53    }
54    
55    setErrors(newErrors);
56    return isValid;
57  };
58  
59  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
60    const { name, value } = e.target;
61    
62    // Aktualizacja danych formularza
63    setFormData(prev => ({
64      ...prev,
65      [name]: value
66    }));
67    
68    // Usunięcie błędu dla edytowanego pola
69    if (errors[name as keyof FormErrors]) {
70      setErrors(prev => {
71        const newErrors = { ...prev };
72        delete newErrors[name as keyof FormErrors];
73        return newErrors;
74      });
75    }
76  };
77  
78  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
79    e.preventDefault();
80    
81    // Resetowanie stanu przed wysyłką
82    setErrors({});
83    setSubmitSuccess(false);
84    
85    // Walidacja formularza
86    if (!validateForm()) {
87      return;
88    }
89    
90    setIsSubmitting(true);
91    
92    try {
93      // Symulacja wysyłki do API z losowym błędem
94      await new Promise(resolve => setTimeout(resolve, 1000));
95      
96      // Symulacja losowego błędu serwera (25% szans)
97      if (Math.random() < 0.25) {
98        throw new Error('Wystąpił błąd podczas przetwarzania formularza');
99      }
100      
101      // Symulacja błędu biznesowego - już istniejący email (25% szans)
102      if (Math.random() < 0.25) {
103        setErrors({
104          email: 'Ten adres email jest już zarejestrowany'
105        });
106        return;
107      }
108      
109      // Sukces
110      setSubmitSuccess(true);
111      setFormData({ name: '', email: '', password: '' });
112      console.log('Formularz wysłany pomyślnie:', formData);
113    } catch (error) {
114      // Obsługa błędu serwera
115      setErrors({
116        generalError: 'Wystąpił problem podczas przetwarzania formularza. Spróbuj ponownie później.'
117      });
118      console.error('Błąd wysyłki formularza:', error);
119    } finally {
120      setIsSubmitting(false);
121    }
122  };
123  
124  return (
125    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
126      <h1 className="text-2xl font-bold mb-6">Rejestracja</h1>
127      
128      {submitSuccess && (
129        <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
130          Rejestracja przebiegła pomyślnie! Sprawdź swój email w celu aktywacji konta.
131        </div>
132      )}
133      
134      {errors.generalError && (
135        <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
136          <p className="font-bold">Wystąpił błąd</p>
137          <p>{errors.generalError}</p>
138        </div>
139      )}
140      
141      <form onSubmit={handleSubmit} className="space-y-4" noValidate>
142        <div>
143          <label htmlFor="name" className="block text-sm font-medium text-gray-700">
144            Imię i nazwisko
145          </label>
146          <input
147            type="text"
148            id="name"
149            name="name"
150            value={formData.name}
151            onChange={handleChange}
152            aria-invalid={Boolean(errors.name)}
153            aria-describedby={errors.name ? 'name-error' : undefined}
154            className={`mt-1 block w-full rounded-md shadow-sm ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
155          />
156          {errors.name && (
157            <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
158              {errors.name}
159            </p>
160          )}
161        </div>
162        
163        <div>
164          <label htmlFor="email" className="block text-sm font-medium text-gray-700">
165            Email
166          </label>
167          <input
168            type="email"
169            id="email"
170            name="email"
171            value={formData.email}
172            onChange={handleChange}
173            aria-invalid={Boolean(errors.email)}
174            aria-describedby={errors.email ? 'email-error' : undefined}
175            className={`mt-1 block w-full rounded-md shadow-sm ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
176          />
177          {errors.email && (
178            <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
179              {errors.email}
180            </p>
181          )}
182        </div>
183        
184        <div>
185          <label htmlFor="password" className="block text-sm font-medium text-gray-700">
186            Hasło
187          </label>
188          <input
189            type="password"
190            id="password"
191            name="password"
192            value={formData.password}
193            onChange={handleChange}
194            aria-invalid={Boolean(errors.password)}
195            aria-describedby={errors.password ? 'password-error' : 'password-rules'}
196            className={`mt-1 block w-full rounded-md shadow-sm ${errors.password ? 'border-red-500' : 'border-gray-300'}`}
197          />
198          {errors.password ? (
199            <p className="mt-1 text-sm text-red-600" id="password-error" role="alert">
200              {errors.password}
201            </p>
202          ) : (
203            <p className="mt-1 text-xs text-gray-500" id="password-rules">
204              Hasło powinno mieć co najmniej 8 znaków, zawierać wielką literę i cyfrę.
205            </p>
206          )}
207        </div>
208        
209        <button
210          type="submit"
211          disabled={isSubmitting}
212          className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
213        >
214          {isSubmitting ? 'Przetwarzanie...' : 'Zarejestruj się'}
215        </button>
216      </form>
217    </div>
218  );
219}

Kluczowe elementy obsługi błędów w tym przykładzie:

  1. Stan błędów - osobny obiekt przechowujący błędy dla poszczególnych pól i ogólny błąd
  2. Atrybuty dostępności -
    aria-invalid
    i
    aria-describedby
    dla czytników ekranu
  3. Dynamiczna walidacja - usuwanie błędów podczas edycji pola
  4. Wskazówki wizualne - czerwona obwódka dla niepoprawnych pól
  5. Komunikaty pomocnicze - wskazówki dotyczące zasad hasła przed wystąpieniem błędu

Obsługa błędów z React Hook Form

React Hook Form oferuje zaawansowane możliwości obsługi błędów:

1import { useForm, SubmitHandler } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4import { useState } from 'react';
5
6// Schemat walidacji
7const signupSchema = z.object({
8  name: z.string().min(1, { message: 'Imię jest wymagane' }),
9  email: z.string().email({ message: 'Podaj prawidłowy adres email' }),
10  password: z.string()
11    .min(8, { message: 'Hasło musi mieć co najmniej 8 znaków' })
12    .regex(/[A-Z]/, { message: 'Hasło musi zawierać co najmniej jedną wielką literę' })
13    .regex(/[0-9]/, { message: 'Hasło musi zawierać co najmniej jedną cyfrę' })
14});
15
16type SignupFormInputs = z.infer<typeof signupSchema>;
17
18export default function HookFormSignup() {
19  const [serverError, setServerError] = useState<string | null>(null);
20  const [submitSuccess, setSubmitSuccess] = useState(false);
21  
22  const { 
23    register, 
24    handleSubmit, 
25    formState: { errors, isSubmitting, touchedFields, isDirty },
26    reset 
27  } = useForm<SignupFormInputs>({
28    resolver: zodResolver(signupSchema),
29    mode: 'onTouched' // Walidacja przy utracie fokusu
30  });
31  
32  const onSubmit: SubmitHandler<SignupFormInputs> = async (data) => {
33    // Resetujemy błędy serwera i sukces przed nową próbą
34    setServerError(null);
35    setSubmitSuccess(false);
36    
37    try {
38      // Symulacja wysyłki do API
39      await new Promise(resolve => setTimeout(resolve, 1000));
40      
41      // Symulacja losowego błędu serwera (25% szans)
42      if (Math.random() < 0.25) {
43        throw new Error('Wystąpił błąd podczas przetwarzania formularza');
44      }
45      
46      // Symulacja błędu biznesowego - już istniejący email (25% szans)
47      if (Math.random() < 0.25) {
48        throw new Error('Ten adres email jest już zarejestrowany');
49      }
50      
51      // Sukces
52      console.log('Formularz wysłany pomyślnie:', data);
53      setSubmitSuccess(true);
54      reset(); // Czyszczenie formularza
55    } catch (error) {
56      if (error instanceof Error) {
57        setServerError(error.message);
58      } else {
59        setServerError('Wystąpił nieznany błąd. Spróbuj ponownie później.');
60      }
61      console.error('Błąd wysyłki formularza:', error);
62    }
63  };
64  
65  return (
66    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
67      <h1 className="text-2xl font-bold mb-6">Rejestracja (React Hook Form)</h1>
68      
69      {submitSuccess && (
70        <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
71          Rejestracja przebiegła pomyślnie! Sprawdź swój email w celu aktywacji konta.
72        </div>
73      )}
74      
75      {serverError && (
76        <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
77          <p className="font-bold">Wystąpił błąd</p>
78          <p>{serverError}</p>
79        </div>
80      )}
81      
82      <form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
83        <div>
84          <label htmlFor="name" className="block text-sm font-medium text-gray-700">
85            Imię i nazwisko
86          </label>
87          <input
88            id="name"
89            type="text"
90            aria-invalid={Boolean(errors.name)}
91            aria-describedby={errors.name ? 'name-error' : undefined}
92            className={`mt-1 block w-full rounded-md shadow-sm ${errors.name ? 'border-red-500' : touchedFields.name ? 'border-green-500' : 'border-gray-300'}`}
93            {...register('name')}
94          />
95          {errors.name && (
96            <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
97              {errors.name.message}
98            </p>
99          )}
100        </div>
101        
102        <div>
103          <label htmlFor="email" className="block text-sm font-medium text-gray-700">
104            Email
105          </label>
106          <input
107            id="email"
108            type="email"
109            aria-invalid={Boolean(errors.email)}
110            aria-describedby={errors.email ? 'email-error' : undefined}
111            className={`mt-1 block w-full rounded-md shadow-sm ${errors.email ? 'border-red-500' : touchedFields.email ? 'border-green-500' : 'border-gray-300'}`}
112            {...register('email')}
113          />
114          {errors.email && (
115            <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
116              {errors.email.message}
117            </p>
118          )}
119        </div>
120        
121        <div>
122          <label htmlFor="password" className="block text-sm font-medium text-gray-700">
123            Hasło
124          </label>
125          <input
126            id="password"
127            type="password"
128            aria-invalid={Boolean(errors.password)}
129            aria-describedby={errors.password ? 'password-error' : 'password-rules'}
130            className={`mt-1 block w-full rounded-md shadow-sm ${errors.password ? 'border-red-500' : touchedFields.password ? 'border-green-500' : 'border-gray-300'}`}
131            {...register('password')}
132          />
133          {errors.password ? (
134            <p className="mt-1 text-sm text-red-600" id="password-error" role="alert">
135              {errors.password.message}
136            </p>
137          ) : (
138            <p className="mt-1 text-xs text-gray-500" id="password-rules">
139              Hasło powinno mieć co najmniej 8 znaków, zawierać wielką literę i cyfrę.
140            </p>
141          )}
142        </div>
143        
144        <button
145          type="submit"
146          disabled={isSubmitting || !isDirty}
147          className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
148        >
149          {isSubmitting ? 'Przetwarzanie...' : 'Zarejestruj się'}
150        </button>
151      </form>
152    </div>
153  );
154}

Kluczowe elementy obsługi błędów w React Hook Form:

  1. Walidacja z Zod - czytelne i typowane schematy walidacji
  2. Wiele stanów formularza -
    isSubmitting
    ,
    touchedFields
    ,
    isDirty
  3. Tryby walidacji - konfiguracja z
    mode: 'onTouched'
  4. Wizualne wskaźniki stanu - zielona obwódka dla poprawnych, czerwona dla błędnych pól
  5. Integracja z ARIA - pełna dostępność forŭmularza

Obsługa błędów w Server Actions (Next.js)

W formularzach korzystających z Server Actions w Next.js, obsługa błędów jest szczególnie istotna, ponieważ procesowanie odbywa się na serwerze:

1// app/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6const contactSchema = z.object({
7  name: z.string().min(1, { message: 'Imię jest wymagane' }),
8  email: z.string().email({ message: 'Podaj prawidłowy adres email' }),
9  subject: z.string().min(3, { message: 'Temat jest zbyt krótki' }),
10  message: z.string().min(10, { message: 'Wiadomość musi mieć co najmniej 10 znaków' })
11});
12
13export type ContactFormState = {
14  errors?: {
15    name?: string[];
16    email?: string[];
17    subject?: string[];
18    message?: string[];
19    _form?: string[];
20  };
21  message?: string;
22  success?: boolean;
23};
24
25export async function submitContactForm(
26  prevState: ContactFormState,
27  formData: FormData
28): Promise<ContactFormState> {
29  // Konwersja FormData na obiekt
30  const rawFormData = {
31    name: formData.get('name'),
32    email: formData.get('email'),
33    subject: formData.get('subject'),
34    message: formData.get('message')
35  };
36  
37  // Walidacja danych
38  const validationResult = contactSchema.safeParse(rawFormData);
39  
40  // Jeśli walidacja nie powiedzie się
41  if (!validationResult.success) {
42    // Konwersja błędów Zod na format dla formularza
43    return {
44      errors: validationResult.error.flatten().fieldErrors,
45      message: 'Proszę poprawić błędy w formularzu',
46      success: false
47    };
48  }
49  
50  try {
51    // Symulacja opóźnienia API
52    await new Promise(resolve => setTimeout(resolve, 1000));
53    
54    // Symulacja losowego błędu serwera (15% szans)
55    if (Math.random() < 0.15) {
56      throw new Error('Problem z połączeniem z serwerem');
57    }
58    
59    // Symulacja błędu spamowego (10% szans)
60    if (Math.random() < 0.1) {
61      return {
62        errors: {
63          _form: ['Wykryto możliwy spam. Spróbuj ponownie później.']
64        },
65        success: false
66      };
67    }
68    
69    // Symulacja przetwarzania formularza
70    console.log('Przetwarzanie formularza kontaktowego:', validationResult.data);
71    
72    return {
73      message: 'Wiadomość została wysłana pomyślnie. Odpowiemy najszybciej jak to możliwe.',
74      success: true
75    };
76  } catch (error) {
77    // Obsługa błędów serwera
78    return {
79      errors: {
80        _form: [
81          'Wystąpił błąd podczas przetwarzania formularza. ' +
82          'Spróbuj ponownie później lub skontaktuj się z nami bezpośrednio.'
83        ]
84      },
85      success: false
86    };
87  }
88}

I komponent używający tej akcji:

1// app/contact/page.tsx
2'use client';
3
4import { useRef, useEffect } from 'react';
5import { useFormState, useFormStatus } from 'react-dom';
6import { submitContactForm, type ContactFormState } from '../actions';
7
8// Komponent przycisku z obsługą stanu wysyłania
9function SubmitButton() {
10  const { pending } = useFormStatus();
11  
12  return (
13    <button
14      type="submit"
15      disabled={pending}
16      aria-disabled={pending}
17      className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
18    >
19      {pending ? 'Wysyłanie...' : 'Wyślij wiadomość'}
20    </button>
21  );
22}
23
24export default function ContactForm() {
25  const formRef = useRef<HTMLFormElement>(null);
26  
27  // Inicjalizacja stanu formularza
28  const initialState: ContactFormState = { errors: {} };
29  
30  // Połączenie z Server Action
31  const [state, formAction] = useFormState(submitContactForm, initialState);
32  
33  // Resetowanie formularza po pomyślnym wysłaniu
34  useEffect(() => {
35    if (state.success && formRef.current) {
36      formRef.current.reset();
37      
38      // Skupienie się na komunikacie sukcesu dla czytników ekranu
39      const successMessage = document.getElementById('success-message');
40      if (successMessage) {
41        successMessage.focus();
42      }
43    }
44  }, [state.success]);
45  
46  return (
47    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
48      <h1 className="text-2xl font-bold mb-6">Skontaktuj się z nami</h1>
49      
50      {state.success && (
51        <div 
52          id="success-message"
53          tabIndex={-1}
54          className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded"
55          role="alert"
56        >
57          {state.message}
58        </div>
59      )}
60      
61      {state.errors?._form && (
62        <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
63          <ul className="list-disc pl-5">
64            {state.errors._form.map((error, index) => (
65              <li key={index}>{error}</li>
66            ))}
67          </ul>
68        </div>
69      )}
70      
71      <form ref={formRef} action={formAction} className="space-y-4" noValidate>
72        <div>
73          <label htmlFor="name" className="block text-sm font-medium text-gray-700">
74            Imię i nazwisko
75          </label>
76          <input
77            type="text"
78            id="name"
79            name="name"
80            aria-invalid={Boolean(state.errors?.name)}
81            aria-describedby={state.errors?.name ? 'name-error' : undefined}
82            className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.name ? 'border-red-500' : 'border-gray-300'}`}
83          />
84          {state.errors?.name && (
85            <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
86              {state.errors.name[0]}
87            </p>
88          )}
89        </div>
90        
91        <div>
92          <label htmlFor="email" className="block text-sm font-medium text-gray-700">
93            Email
94          </label>
95          <input
96            type="email"
97            id="email"
98            name="email"
99            aria-invalid={Boolean(state.errors?.email)}
100            aria-describedby={state.errors?.email ? 'email-error' : undefined}
101            className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.email ? 'border-red-500' : 'border-gray-300'}`}
102          />
103          {state.errors?.email && (
104            <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
105              {state.errors.email[0]}
106            </p>
107          )}
108        </div>
109        
110        <div>
111          <label htmlFor="subject" className="block text-sm font-medium text-gray-700">
112            Temat
113          </label>
114          <input
115            type="text"
116            id="subject"
117            name="subject"
118            aria-invalid={Boolean(state.errors?.subject)}
119            aria-describedby={state.errors?.subject ? 'subject-error' : undefined}
120            className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.subject ? 'border-red-500' : 'border-gray-300'}`}
121          />
122          {state.errors?.subject && (
123            <p className="mt-1 text-sm text-red-600" id="subject-error" role="alert">
124              {state.errors.subject[0]}
125            </p>
126          )}
127        </div>
128        
129        <div>
130          <label htmlFor="message" className="block text-sm font-medium text-gray-700">
131            Wiadomość
132          </label>
133          <textarea
134            id="message"
135            name="message"
136            rows={4}
137            aria-invalid={Boolean(state.errors?.message)}
138            aria-describedby={state.errors?.message ? 'message-error' : undefined}
139            className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.message ? 'border-red-500' : 'border-gray-300'}`}
140          />
141          {state.errors?.message && (
142            <p className="mt-1 text-sm text-red-600" id="message-error" role="alert">
143              {state.errors.message[0]}
144            </p>
145          )}
146        </div>
147        
148        <SubmitButton />
149      </form>
150    </div>
151  );
152}

Kluczowe elementy obsługi błędów z Server Actions:

  1. Struktura błędów - specjalny format z błędami dla pól i ogólnym błędem formularza
  2. useFormState i useFormStatus - hooki do śledzenia stanu formularza i procesu wysyłki
  3. Focus po sukcesie - przesunięcie fokusu do komunikatu sukcesu dla użytkowników czytników ekranu
  4. Struktura danych zwrotnych - spójny format odpowiedzi z serwera
  5. Wiele rodzajów błędów - różne scenariusze błędów dla różnych przyczyn

Zaawansowane wzorce obsługi błędów

Konteksty błędów formularza

Dla bardziej złożonych formularzy, warto rozważyć utworzenie kontekstu błędów:

1// contexts/FormErrorContext.tsx
2import { createContext, useContext, ReactNode, useState } from 'react';
3
4type ErrorsType = Record<string, string[]>;
5
6interface FormErrorContextType {
7  errors: ErrorsType;
8  setErrors: (errors: ErrorsType) => void;
9  clearErrors: () => void;
10  clearFieldError: (fieldName: string) => void;
11  hasErrors: boolean;
12  getFieldErrors: (fieldName: string) => string[] | undefined;
13}
14
15const FormErrorContext = createContext<FormErrorContextType | null>(null);
16
17export function FormErrorProvider({ children }: { children: ReactNode }) {
18  const [errors, setErrors] = useState<ErrorsType>({});
19  
20  const clearErrors = () => setErrors({});
21  
22  const clearFieldError = (fieldName: string) => {
23    setErrors(prev => {
24      const newErrors = { ...prev };
25      delete newErrors[fieldName];
26      return newErrors;
27    });
28  };
29  
30  const getFieldErrors = (fieldName: string) => errors[fieldName];
31  
32  const hasErrors = Object.keys(errors).length > 0;
33  
34  return (
35    <FormErrorContext.Provider value={{ 
36      errors, 
37      setErrors, 
38      clearErrors, 
39      clearFieldError, 
40      hasErrors,
41      getFieldErrors 
42    }}>
43      {children}
44    </FormErrorContext.Provider>
45  );
46}
47
48export function useFormErrors() {
49  const context = useContext(FormErrorContext);
50  if (!context) {
51    throw new Error('useFormErrors must be used within a FormErrorProvider');
52  }
53  return context;
54}

Komponent błędu formularza (reużywalny)

1// components/FormError.tsx
2import { useId } from 'react';
3
4interface FormErrorProps {
5  errors?: string[];
6  id?: string;
7}
8
9export function FormError({ errors, id: propId }: FormErrorProps) {
10  const generatedId = useId();
11  const id = propId || generatedId;
12  
13  if (!errors || errors.length === 0) {
14    return null;
15  }
16  
17  return (
18    <div className="mt-1" id={id} role="alert">
19      {errors.map((error, index) => (
20        <p key={index} className="text-sm text-red-600">
21          {error}
22        </p>
23      ))}
24    </div>
25  );
26}

Komponent pola formularza (reużywalny)

1// components/FormField.tsx
2import { InputHTMLAttributes, ReactNode, useId } from 'react';
3import { FormError } from './FormError';
4import { useFormErrors } from '../contexts/FormErrorContext';
5
6interface FormFieldProps extends InputHTMLAttributes<HTMLInputElement> {
7  label: string;
8  name: string;
9  errors?: string[];
10  children?: ReactNode;
11  helpText?: string;
12}
13
14export function FormField({ 
15  label, 
16  name, 
17  errors: propErrors, 
18  children, 
19  helpText,
20  ...rest 
21}: FormFieldProps) {
22  const fieldId = useId();
23  const errorId = `${fieldId}-error`;
24  const helpId = `${fieldId}-help`;
25  
26  // Możemy użyć kontekstu błędów, jeśli błędy nie są przekazane jako prop
27  const { getFieldErrors } = useFormErrors();
28  const errors = propErrors || getFieldErrors(name);
29  
30  const hasError = errors && errors.length > 0;
31  
32  // Określ, który element opisuje to pole (błąd lub tekst pomocniczy)
33  const ariaDescribedBy = hasError ? errorId : helpText ? helpId : undefined;
34  
35  return (
36    <div>
37      <label htmlFor={fieldId} className="block text-sm font-medium text-gray-700">
38        {label}
39      </label>
40      
41      <div className="mt-1">
42        {children ? (
43          // Jeśli przekazano dzieci (np. niestandardowe pola)
44          children
45        ) : (
46          // Domyślnie renderuj inputa
47          <input
48            id={fieldId}
49            name={name}
50            aria-invalid={hasError}
51            aria-describedby={ariaDescribedBy}
52            className={`block w-full rounded-md shadow-sm ${hasError ? 'border-red-500' : 'border-gray-300'}`}
53            {...rest}
54          />
55        )}
56      </div>
57      
58      {hasError ? (
59        // Pokaż błędy
60        <FormError errors={errors} id={errorId} />
61      ) : helpText ? (
62        // Pokaż tekst pomocniczy jeśli nie ma błędów
63        <p className="mt-1 text-xs text-gray-500" id={helpId}>
64          {helpText}
65        </p>
66      ) : null}
67    </div>
68  );
69}

Przykład użycia reużywalnych komponentów

1// app/registration/page.tsx
2import { FormErrorProvider } from '../contexts/FormErrorContext';
3import { FormField } from '../components/FormField';
4
5export default function RegistrationPage() {
6  return (
7    <FormErrorProvider>
8      <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
9        <h1 className="text-2xl font-bold mb-6">Rejestracja</h1>
10        
11        <form className="space-y-4" noValidate>
12          <FormField
13            label="Imię i nazwisko"
14            name="name"
15            type="text"
16            required
17            helpText="Wprowadź swoje pełne imię i nazwisko"
18          />
19          
20          <FormField
21            label="Email"
22            name="email"
23            type="email"
24            required
25            helpText="Ten adres będzie służył do logowania do konta"
26          />
27          
28          <FormField
29            label="Hasło"
30            name="password"
31            type="password"
32            required
33            helpText="Hasło powinno mieć co najmniej 8 znaków, zawierać wielką literę i cyfrę"
34          />
35          
36          {/* Niestandardowe pole z własną implementacją */}
37          <FormField
38            label="Kategoria konta"
39            name="accountType"
40            helpText="Wybierz typ konta odpowiedni dla Twoich potrzeb"
41          >
42            <div className="flex space-x-4">
43              <label className="inline-flex items-center">
44                <input type="radio" name="accountType" value="personal" className="mr-2" />
45                Osobiste
46              </label>
47              <label className="inline-flex items-center">
48                <input type="radio" name="accountType" value="business" className="mr-2" />
49                Biznesowe
50              </label>
51            </div>
52          </FormField>
53          
54          <button
55            type="submit"
56            className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700"
57          >
58            Zarejestruj się
59          </button>
60        </form>
61      </div>
62    </FormErrorProvider>
63  );
64}

Strategie obsługi błędów formularzy i ich wpływ na UX

Różne strategie obsługi błędów mogą znacząco wpłynąć na doświadczenie użytkownika:

1. Walidacja w czasie rzeczywistym vs przy wysyłce

Walidacja w czasie rzeczywistym:

  • Zalety: Natychmiastowa informacja zwrotna, mniej frustracji przy wysyłce
  • Wady: Może być przytłaczająca i irytująca, jeśli pojawia się zbyt wcześnie

Walidacja przy wysyłce:

  • Zalety: Mniej rozpraszająca, pozwala użytkownikowi najpierw wprowadzić dane
  • Wady: Może być frustrująca, jeśli użytkownik wypełnia długi formularz, a potem musi wracać do poprawek

Najlepsze podejście: Kompromis - walidacja przy utracie fokusu lub po pierwszej próbie wysyłki

2. Walidacja po stronie klienta vs serwera

Walidacja po stronie klienta:

  • Zalety: Szybka, bez opóźnienia sieciowego, oszczędza zasoby serwera
  • Wady: Może zostać ominięta, nie zawsze odzwierciedla faktyczne reguły biznesowe

Walidacja po stronie serwera:

  • Zalety: Niezawodna, spójna, uwzględnia aktualny stan systemu
  • Wady: Opóźniona informacja zwrotna, większe obciążenie serwera

Najlepsze podejście: Walidacja na obu poziomach - klient dla UX, serwer dla bezpieczeństwa

3. Walidacja pól vs formularza jako całości

Walidacja poszczególnych pól:

  • Zalety: Precyzyjna informacja o błędach, łatwiejsze poprawianie
  • Wady: Nie uwzględnia zależności między polami

Walidacja całego formularza:

  • Zalety: Może uwzględniać zależności między polami
  • Wady: Może być przytłaczająca, jeśli formularz ma wiele błędów

Najlepsze podejście: Walidacja pól podczas wprowadzania i pełna walidacja przy wysyłce

Dobre praktyki obsługi błędów formularzy

  1. Grupuj powiązane błędy - błędy dotyczące całego formularza umieszczaj na górze, błędy konkretnych pól - przy polach

  2. Zachowaj spójność wizualną - używaj tych samych kolorów, ikon i styli dla błędów w całej aplikacji

  3. Doskonal komunikaty - zamiast "Nieprawidłowe dane" napisz dokładnie, co jest nie tak i jak to naprawić

  4. Zachowaj dane przy błędach - nie resetuj całego formularza po wystąpieniu błędu

  5. Priorytetyzuj błędy - najpierw pokaż krytyczne błędy, potem mniej istotne

  6. Automatycznie przewijaj do błędów - szczególnie w długich formularzach, automatycznie przewijaj do pierwszego błędu

  7. Wykorzystuj ARIA - używaj atrybutów

    aria-invalid
    ,
    aria-describedby
    i role="alert" dla dostępności

  8. Oznaczaj wymagane pola - jasno oznacz, które pola są wymagane, najlepiej przed wypełnianiem formularza

  9. Podawaj przykłady - dla skomplikowanych formatów (np. data, numer telefonu) podawaj przykłady

  10. Testuj z użytkownikami - regularnie testuj formularze z rzeczywistymi użytkownikami, aby zidentyfikować problemy

Obsługa błędów a dostępność

Efektywna obsługa błędów jest szczególnie ważna dla użytkowników z niepełnosprawnościami:

  1. Używaj atrybutów ARIA

    • aria-invalid="true"
      dla niepoprawnych pól
    • aria-describedby
      łączący pole z komunikatem błędu
    • role="alert"
      dla komunikatów błędów, które powinny być natychmiast odczytane
  2. Nie polegaj tylko na kolorach

    • Dodaj ikony lub inne wskaźniki wizualne oprócz koloru czerwonego
    • Zapewnij wystarczający kontrast między tekstem a tłem
  3. Używaj

    <fieldset>
    i
    <legend>

    • Grupuj powiązane pola za pomocą
      <fieldset>
    • Opisuj grupy pól za pomocą
      <legend>
  4. Prawidłowa kolejność tabulacji

    • Upewnij się, że użytkownicy klawiatury mogą poruszać się między polami w logicznej kolejności
  5. Podsumowanie błędów

    • Dla złożonych formularzy, dodaj podsumowanie błędów na górze formularza
    • Każdy błąd powinien zawierać link do odpowiedniego pola

Podsumowanie

Efektywna obsługa błędów formularzy to znacznie więcej niż tylko informowanie użytkownika o problemach. To kompleksowe podejście, łączące:

  1. Projektowanie przyjaznych komunikatów - jasne, konkretne i pomocne
  2. Walidację na wielu poziomach - klient + serwer, poszczególne pola + cały formularz
  3. Odpowiedni timing - wybieranie optymalnego momentu na pokaźnie błędów
  4. Dostępność - zapewnienie, że wszyscy użytkownicy mogą zrozumieć i poprawić błędy
  5. Spójność wizualną - jednolite style i zachowania w całej aplikacji

Pamiętaj, że cel to nie tylko wykrycie błędów, ale przede wszystkim pomoc użytkownikowi w ich skutecznym poprawieniu i pomyślnym ukończeniu formularza. Dobrze zaprojektowana obsługa błędów może znacząco poprawić współczynnik konwersji i satysfakcję użytkowników.

Vai a CodeWorlds