Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense
Obsługa błędów w formularzach
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ć:
- Konkretne -- "Hasło musi mieć min. 8 znaków" zamiast "Nieprawidłowe hasło"
- Pomocne -- powiedz użytkownikowi JAK naprawić błąd
- Widoczne -- wyświetlaj błąd blisko pola, którego dotyczy
- Delikatne -- nie wyświetlaj wszystkich błędów na raz przed pierwszym submit
- Dostępne -- używaj atrybutów
aria-invalidiaria-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. Jaka jest główna zaleta walidacji formularzy po stronie klienta (client-side)?
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: