Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense

Obsługa błędów w formularzach

5 min czytania
W tej lekcji5

Wyobraź sobie, że twoja stacja kosmiczna ma panel rejestracji nowych członków załogi. Każde pole formularza to jak czujnik na statku -- jeśli dane są niepoprawne, system musi natychmiast powiadomić operatora zanim sekwencja startowa zostanie uruchomiona. Obsługa błędów w formularzach to kluczowy element UX każdej aplikacji React.

Wzorce walidacji formularzy

W React możemy wyróżnić dwa główne podejścia do walidacji formularzy:

1. Walidacja po stronie klienta (client-side)

Walidacja wykonywana w przeglądarce, zanim dane zostaną wysłane na serwer. Jest szybka i daje natychmiastowy feedback.

1function RegistrationForm() {
2  const [email, setEmail] = useState('');
3  const [errors, setErrors] = useState({});
4
5  const validateEmail = (value) => {
6    if (!value) return 'Email jest wymagany';
7    if (!value.includes('@')) return 'Nieprawidłowy format email';
8    return null;
9  };
10
11  const handleChange = (e) => {
12    const value = e.target.value;
13    setEmail(value);
14    const error = validateEmail(value);
15    setErrors(prev => ({ ...prev, email: error }));
16  };
17
18  return (
19    <div>
20      <input value={email} onChange={handleChange} />
21      {errors.email && <p className="error">{errors.email}</p>}
22    </div>
23  );
24}

2. Walidacja po stronie serwera (server-side)

Serwer sprawdza dane po wysłaniu formularza i zwraca błędy. Jest niezbędna dla bezpieczeństwa -- walidacja kliencka może zostać pominięta.

1const handleSubmit = async (formData) => {
2  try {
3    const response = await fetch('/api/register', {
4      method: 'POST',
5      body: JSON.stringify(formData),
6    });
7    const result = await response.json();
8
9    if (!response.ok) {
10      // Serwer zwrócił błędy walidacji
11      setErrors(result.errors);
12    }
13  } catch (error) {
14    setErrors({ general: 'Błąd połączenia z serwerem' });
15  }
16};

Wyświetlanie błędów na poziomie pól (field-level errors)

Każde pole formularza powinno mieć swój własny komunikat błędu, wyświetlany bezpośrednio pod polem. To jak indywidualne alarmy dla każdego systemu na statku kosmicznym.

1function CrewRegistration() {
2  const [formData, setFormData] = useState({
3    name: '', callsign: '', rank: ''
4  });
5  const [errors, setErrors] = useState({});
6  const [touched, setTouched] = useState({});
7
8  const validate = (field, value) => {
9    switch (field) {
10      case 'name':
11        if (!value.trim()) return 'Imię jest wymagane';
12        if (value.length < 2) return 'Imię musi mieć min. 2 znaki';
13        return null;
14      case 'callsign':
15        if (!value.trim()) return 'Znak wywoławczy jest wymagany';
16        if (!/^[A-Z0-9-]+$/.test(value)) return 'Tylko wielkie litery, cyfry i myślniki';
17        return null;
18      case 'rank':
19        if (!value) return 'Wybierz rangę';
20        return null;
21      default:
22        return null;
23    }
24  };
25
26  const handleBlur = (field) => {
27    setTouched(prev => ({ ...prev, [field]: true }));
28    const error = validate(field, formData[field]);
29    setErrors(prev => ({ ...prev, [field]: error }));
30  };
31
32  const handleChange = (field, value) => {
33    setFormData(prev => ({ ...prev, [field]: value }));
34    // Waliduj tylko jeżeli pole było już dotykane
35    if (touched[field]) {
36      const error = validate(field, value);
37      setErrors(prev => ({ ...prev, [field]: error }));
38    }
39  };
40
41  return (
42    <form>
43      <div>
44        <label>Imię</label>
45        <input
46          value={formData.name}
47          onChange={(e) => handleChange('name', e.target.value)}
48          onBlur={() => handleBlur('name')}
49          className={errors.name ? 'input-error' : ''}
50        />
51        {touched.name && errors.name && (
52          <p className="field-error">{errors.name}</p>
53        )}
54      </div>
55      {/* Analogicznie dla pozostałych pól */}
56    </form>
57  );
58}

Zwróć uwagę na wzorzec touched -- błędy wyświetlamy dopiero po tym, jak użytkownik opuścił pole (onBlur). Nie chcemy straszyć pilota alarmami zanim jeszcze skończył wpisywać dane!

Obsługa błędów serwera w formularzach

Serwer może zwrócić błędy, których nie da się przewidzieć po stronie klienta -- np. "ten email jest już zajęty" albo "przekroczono limit rejestracji".

1function SubmitWithServerErrors() {
2  const [errors, setErrors] = useState({});
3  const [serverError, setServerError] = useState(null);
4
5  const handleSubmit = async (e) => {
6    e.preventDefault();
7    setServerError(null);
8
9    try {
10      const response = await fetch('/api/crew/register', {
11        method: 'POST',
12        headers: { 'Content-Type': 'application/json' },
13        body: JSON.stringify(formData),
14      });
15
16      const data = await response.json();
17
18      if (!response.ok) {
19        if (data.fieldErrors) {
20          // Błędy konkretnych pól (np. "email już istnieje")
21          setErrors(data.fieldErrors);
22        }
23        if (data.message) {
24          // Ogólny błąd serwera
25          setServerError(data.message);
26        }
27        return;
28      }
29
30      // Sukces - przekierowanie lub komunikat
31    } catch (error) {
32      setServerError('Nie udało się połączyć ze stacją bazową');
33    }
34  };
35
36  return (
37    <form onSubmit={handleSubmit}>
38      {serverError && (
39        <div className="server-error-banner">{serverError}</div>
40      )}
41      {/* Pola formularza z field-level errors */}
42    </form>
43  );
44}

try-catch w handleSubmit

Każdy handler wysyłania formularza powinien być opakowany w try-catch. Bez tego błąd sieciowy lub nieoczekiwany błąd serwera spowoduje nieobsłużony wyjątek.

1const handleSubmit = async (e) => {
2  e.preventDefault();
3  setIsSubmitting(true);
4  setErrors({});
5
6  try {
7    // 1. Walidacja lokalna
8    const validationErrors = validateAllFields(formData);
9    if (Object.keys(validationErrors).length > 0) {
10      setErrors(validationErrors);
11      return;
12    }
13
14    // 2. Wysłanie do serwera
15    const response = await submitToServer(formData);
16
17    // 3. Obsługa odpowiedzi serwera
18    if (!response.ok) {
19      const errorData = await response.json();
20      throw new Error(errorData.message || 'Błąd serwera');
21    }
22
23    // 4. Sukces
24    setSuccess(true);
25  } catch (error) {
26    // 5. Obsługa błędów
27    setErrors({ submit: error.message });
28  } finally {
29    // 6. Zawsze wykonane - wyłącz loader
30    setIsSubmitting(false);
31  }
32};

UX best practices dla komunikatów błędów

Dobre komunikaty błędów w formularzach powinny być:

  1. Konkretne -- "Hasło musi mieć min. 8 znaków" zamiast "Nieprawidłowe hasło"
  2. Pomocne -- powiedz użytkownikowi JAK naprawić błąd
  3. Widoczne -- wyświetlaj błąd blisko pola, którego dotyczy
  4. Delikatne -- nie wyświetlaj wszystkich błędów na raz przed pierwszym submit
  5. Dostępne -- używaj atrybutów aria-invalid i aria-describedby
1<div className="form-field">
2  <label htmlFor="password">Hasło</label>
3  <input
4    id="password"
5    type="password"
6    aria-invalid={!!errors.password}
7    aria-describedby={errors.password ? 'password-error' : undefined}
8  />
9  {errors.password && (
10    <p id="password-error" className="field-error" role="alert">
11      {errors.password}
12    </p>
13  )}
14</div>

Pamiętaj -- formularz to jak panel sterowania statku kosmicznego. Każdy błąd musi być komunikowany jasno i precyzyjnie, aby pilot (użytkownik) wiedział dokładnie, co poprawić. Nigdy nie pokazuj tajemniczych kodów błędów -- zawsze tłumacz je na język zrozumiały dla człowieka.

Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Formularz rejestracji czlonka zalogi z walidacja
5function CrewRegistrationForm() {
6  const [formData, setFormData] = useState({
7    name: '', callsign: '', email: '', rank: ''
8  });
9  const [errors, setErrors] = useState({});
10  const [touched, setTouched] = useState({});
11  const [serverError, setServerError] = useState(null);
12  const [isSubmitting, setIsSubmitting] = useState(false);
13  const [success, setSuccess] = useState(false);
14
15  const validate = (field, value) => {
16    switch (field) {
17      case 'name':
18        if (!value.trim()) return 'Imie jest wymagane';
19        if (value.length < 2) return 'Imie musi miec min. 2 znaki';
20        return null;
21      case 'callsign':
22        if (!value.trim()) return 'Znak wywolawczy jest wymagany';
23        if (!/^[A-Za-z0-9-]+$/.test(value))
24          return 'Tylko litery, cyfry i myslniki';
25        return null;
26      case 'email':
27        if (!value.trim()) return 'Email jest wymagany';
28        if (!value.includes('@')) return 'Nieprawidlowy format email';
29        return null;
30      case 'rank':
31        if (!value) return 'Wybierz range';
32        return null;
33      default:
34        return null;
35    }
36  };
37
38  const handleChange = (field, value) => {
39    setFormData(prev => ({ ...prev, [field]: value }));
40    if (touched[field]) {
41      const error = validate(field, value);
42      setErrors(prev => ({ ...prev, [field]: error }));
43    }
44  };
45
46  const handleBlur = (field) => {
47    setTouched(prev => ({ ...prev, [field]: true }));
48    const error = validate(field, formData[field]);
49    setErrors(prev => ({ ...prev, [field]: error }));
50  };
51
52  const handleSubmit = async (e) => {
53    e.preventDefault();
54    setServerError(null);
55    setIsSubmitting(true);
56
57    // Walidacja wszystkich pol
58    const allErrors = {};
59    const allTouched = {};
60    for (const field of Object.keys(formData)) {
61      allTouched[field] = true;
62      const error = validate(field, formData[field]);
63      if (error) allErrors[field] = error;
64    }
65    setTouched(allTouched);
66    setErrors(allErrors);
67
68    if (Object.keys(allErrors).length > 0) {
69      setIsSubmitting(false);
70      return;
71    }
72
73    try {
74      // Symulacja wyslania do serwera
75      await new Promise(resolve => setTimeout(resolve, 1000));
76
77      // Symulacja bledu serwera (50% szans)
78      if (Math.random() > 0.5) {
79        throw new Error('Serwer odrzucil rejestracje: callsign juz istnieje');
80      }
81
82      setSuccess(true);
83    } catch (error) {
84      setServerError(error.message);
85    } finally {
86      setIsSubmitting(false);
87    }
88  };
89
90  if (success) {
91    return (
92      <div className="success-panel">
93        <h2>Rejestracja udana!</h2>
94        <p>Witamy na pokladzie, {formData.callsign}!</p>
95        <button onClick={() => { setSuccess(false); setFormData({ name: '', callsign: '', email: '', rank: '' }); setTouched({}); }}>
96          Zarejestruj kolejnego
97        </button>
98      </div>
99    );
100  }
101
102  return (
103    <form onSubmit={handleSubmit} className="form">
104      {serverError && (
105        <div className="server-error">{serverError}</div>
106      )}
107
108      <div className="field">
109        <label>Imie</label>
110        <input
111          value={formData.name}
112          onChange={(e) => handleChange('name', e.target.value)}
113          onBlur={() => handleBlur('name')}
114          className={touched.name && errors.name ? 'input-error' : ''}
115          placeholder="Np. Jan Kowalski"
116        />
117        {touched.name && errors.name && (
118          <p className="field-error">{errors.name}</p>
119        )}
120      </div>
121
122      <div className="field">
123        <label>Znak wywolawczy</label>
124        <input
125          value={formData.callsign}
126          onChange={(e) => handleChange('callsign', e.target.value)}
127          onBlur={() => handleBlur('callsign')}
128          className={touched.callsign && errors.callsign ? 'input-error' : ''}
129          placeholder="Np. NOVA-7"
130        />
131        {touched.callsign && errors.callsign && (
132          <p className="field-error">{errors.callsign}</p>
133        )}
134      </div>
135
136      <div className="field">
137        <label>Email</label>
138        <input
139          value={formData.email}
140          onChange={(e) => handleChange('email', e.target.value)}
141          onBlur={() => handleBlur('email')}
142          className={touched.email && errors.email ? 'input-error' : ''}
143          placeholder="Np. jan@stacja.kosmos"
144        />
145        {touched.email && errors.email && (
146          <p className="field-error">{errors.email}</p>
147        )}
148      </div>
149
150      <div className="field">
151        <label>Ranga</label>
152        <select
153          value={formData.rank}
154          onChange={(e) => handleChange('rank', e.target.value)}
155          onBlur={() => handleBlur('rank')}
156          className={touched.rank && errors.rank ? 'input-error' : ''}
157        >
158          <option value="">-- Wybierz range --</option>
159          <option value="cadet">Kadet</option>
160          <option value="officer">Oficer</option>
161          <option value="commander">Komandor</option>
162        </select>
163        {touched.rank && errors.rank && (
164          <p className="field-error">{errors.rank}</p>
165        )}
166      </div>
167
168      <button type="submit" className="submit-btn" disabled={isSubmitting}>
169        {isSubmitting ? 'Wysylanie...' : 'Zarejestruj czlonka zalogi'}
170      </button>
171    </form>
172  );
173}
174
175export default function App() {
176  return (
177    <div className="app">
178      <h1>Rejestracja Zalogi</h1>
179      <p className="subtitle">Walidacja formularza z obsluga bledow</p>
180      <CrewRegistrationForm />
181    </div>
182  );
183}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka jest główna zaleta walidacji formularzy po stronie klienta (client-side)?

  2. 2. Po co używamy wzorca touched (onBlur) przy wyświetlaniu błędów walidacji?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Kliknij elementy w kolejności kroków handleSubmit formularza:

  • Edytor kodu

    Zaimplementuj formularz z walidacją i obsługą błędów

  • Układanie w poziomie

    Ułóż poprawną składnię warunkowego wyświetlania błędu pola:

  • Układanie w pionie

    Uporządkuj typy błędów formularza od obsługiwanych najwcześniej do najpóźniej:

Przydatne artykuły