Kurs JavaScript i React · Moduł 8: Zaawansowane wzorce React
Accessibility (a11y) w React
W tej lekcji9
Na stacji kosmicznej każdy członek załogi musi mieć dostęp do systemów sterowania - niezależnie od tego, czy obsługuje je wzrokiem, dotykiem czy komendami głosowymi. Dokładnie tak samo działa dostępność (accessibility, w skrócie a11y) w aplikacjach webowych. Budując interfejs w React, musimy zadbać o to, by każdy użytkownik mógł z niego korzystać - osoby niewidome używające czytników ekranu, osoby poruszające się tylko klawiaturą, czy osoby z zaburzeniami widzenia kolorów.
Dlaczego dostępność jest ważna?
Według WHO, ponad miliard ludzi na świecie żyje z jakąś formą niepełnosprawności. To oznacza, że aplikacja niedostępna wyklucza ogromną grupę potencjalnych użytkowników. Poza aspektem etycznym, dostępność:
- Jest wymagana prawnie w wielu krajach (WCAG, ADA)
- Poprawia SEO i ogólną jakość kodu
- Ułatwia korzystanie z aplikacji WSZYSTKIM użytkownikom (np. na urządzeniach mobilnych)
- Sprawia, że interfejs jest bardziej przewidywalny i testowalny
Semantyczny HTML w React
Fundamentem dostępności jest używanie właściwych elementów HTML. React zachęca do tworzenia komponentów, ale łatwo wpaść w pułapkę nadużywania <div>:
1// ŹLE - "div soup" - czytnik ekranu nie rozumie struktury
2function BadNavigation() {
3 return (
4 <div className="nav">
5 <div className="nav-item" onClick={goHome}>Start</div>
6 <div className="nav-item" onClick={goMissions}>Misje</div>
7 <div className="nav-item" onClick={goCrewPanel}>Załoga</div>
8 </div>
9 );
10}
11
12// DOBRZE - semantyczne elementy HTML
13function GoodNavigation() {
14 return (
15 <nav aria-label="Menu główne stacji">
16 <ul>
17 <li><a href="/">Start</a></li>
18 <li><a href="/missions">Misje</a></li>
19 <li><a href="/crew">Załoga</a></li>
20 </ul>
21 </nav>
22 );
23}Kluczowe semantyczne elementy to: <nav>, <main>, <header>, <footer>, <section>, <article>, <aside>, <button>, <form>, <label>.
Atrybuty ARIA w React
ARIA (Accessible Rich Internet Applications) to zbiór atrybutów, które dodają informacje o roli i stanie elementów dla technologii asystujących. W React używamy ich w formacie camelCase lub z myślnikami:
1function MissionAlert({ message, severity }) {
2 return (
3 <div
4 role="alert"
5 aria-live="assertive"
6 aria-atomic="true"
7 className={severity}
8 >
9 {message}
10 </div>
11 );
12}
13
14function SearchField() {
15 const [results, setResults] = useState([]);
16 const [isOpen, setIsOpen] = useState(false);
17
18 return (
19 <div>
20 <label htmlFor="galaxy-search">Szukaj galaktyk:</label>
21 <input
22 id="galaxy-search"
23 type="search"
24 role="combobox"
25 aria-expanded={isOpen}
26 aria-controls="search-results"
27 aria-autocomplete="list"
28 aria-haspopup="listbox"
29 />
30 {isOpen && (
31 <ul id="search-results" role="listbox">
32 {results.map(r => (
33 <li key={r.id} role="option">{r.name}</li>
34 ))}
35 </ul>
36 )}
37 </div>
38 );
39}Najczęściej używane atrybuty ARIA w React:
| Atrybut | Zastosowanie |
|---|---|
role | Definiuje rolę elementu (alert, dialog, tabpanel) |
aria-label | Tekstowa etykieta dla elementu |
aria-labelledby | ID elementu, który jest etykietą |
aria-describedby | ID elementu z dodatkowym opisem |
aria-live | Ogłasza dynamiczne zmiany treści |
aria-expanded | Czy element jest rozwinięty |
aria-hidden | Ukrywa element przed czytnikiem |
aria-disabled | Wskazuje element nieaktywny |
Zarządzanie fokusem
W aplikacjach SPA (Single Page Application) zarządzanie fokusem klawiatury jest krytyczne. Gdy użytkownik nawiguje klawiaturą, fokus musi przesuwać się w logicznej kolejności:
1import { useRef, useEffect } from 'react';
2
3function MissionModal({ isOpen, onClose, title, children }) {
4 const modalRef = useRef(null);
5 const closeButtonRef = useRef(null);
6
7 // Przenieś fokus do modala po otwarciu
8 useEffect(() => {
9 if (isOpen && closeButtonRef.current) {
10 closeButtonRef.current.focus();
11 }
12 }, [isOpen]);
13
14 // Pułapka fokusa - Tab nie wychodzi poza modal
15 const handleKeyDown = (e) => {
16 if (e.key === 'Escape') {
17 onClose();
18 }
19
20 if (e.key === 'Tab') {
21 const focusableElements = modalRef.current.querySelectorAll(
22 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
23 );
24 const firstElement = focusableElements[0];
25 const lastElement = focusableElements[focusableElements.length - 1];
26
27 if (e.shiftKey && document.activeElement === firstElement) {
28 e.preventDefault();
29 lastElement.focus();
30 } else if (!e.shiftKey && document.activeElement === lastElement) {
31 e.preventDefault();
32 firstElement.focus();
33 }
34 }
35 };
36
37 if (!isOpen) return null;
38
39 return (
40 <div
41 className="modal-overlay"
42 role="dialog"
43 aria-modal="true"
44 aria-labelledby="modal-title"
45 ref={modalRef}
46 onKeyDown={handleKeyDown}
47 >
48 <div className="modal-content">
49 <h2 id="modal-title">{title}</h2>
50 {children}
51 <button ref={closeButtonRef} onClick={onClose}>
52 Zamknij
53 </button>
54 </div>
55 </div>
56 );
57}Nawigacja klawiaturą
Każdy interaktywny element musi być dostępny z klawiatury. React wspiera obsługę klawiszy przez onKeyDown, onKeyUp i onKeyPress:
1function SpaceshipSelector({ ships, onSelect }) {
2 const [activeIndex, setActiveIndex] = useState(0);
3
4 const handleKeyDown = (e) => {
5 switch (e.key) {
6 case 'ArrowDown':
7 e.preventDefault();
8 setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
9 break;
10 case 'ArrowUp':
11 e.preventDefault();
12 setActiveIndex(prev => Math.max(prev - 1, 0));
13 break;
14 case 'Enter':
15 case ' ':
16 e.preventDefault();
17 onSelect(ships[activeIndex]);
18 break;
19 }
20 };
21
22 return (
23 <ul
24 role="listbox"
25 aria-label="Wybierz statek kosmiczny"
26 tabIndex={0}
27 onKeyDown={handleKeyDown}
28 >
29 {ships.map((ship, index) => (
30 <li
31 key={ship.id}
32 role="option"
33 aria-selected={index === activeIndex}
34 onClick={() => onSelect(ship)}
35 style={{
36 background: index === activeIndex ? '#1a3a5c' : 'transparent',
37 }}
38 >
39 {ship.name} - {ship.type}
40 </li>
41 ))}
42 </ul>
43 );
44}Ogłaszanie zmian dynamicznych (Live Regions)
Gdy treść strony zmienia się dynamicznie (np. pojawia się powiadomienie), czytnik ekranu musi o tym wiedzieć. Używamy aria-live:
1function MissionStatusBoard() {
2 const [status, setStatus] = useState('Oczekiwanie na start');
3 const [logs, setLogs] = useState([]);
4
5 const updateStatus = (newStatus) => {
6 setStatus(newStatus);
7 setLogs(prev => [...prev, newStatus]);
8 };
9
10 return (
11 <div>
12 {/* aria-live="polite" - czeka aż czytnik skończy bieżący tekst */}
13 <div aria-live="polite" aria-atomic="true">
14 <h2>Status misji: {status}</h2>
15 </div>
16
17 {/* aria-live="assertive" - przerywa bieżące czytanie */}
18 <div aria-live="assertive" className="sr-only">
19 {logs.length > 0 && logs[logs.length - 1]}
20 </div>
21
22 <button onClick={() => updateStatus('Odliczanie rozpoczęte')}>
23 Rozpocznij odliczanie
24 </button>
25 <button onClick={() => updateStatus('Start!')}>
26 Start
27 </button>
28 </div>
29 );
30}Klasa sr-only (screen reader only)
Czasami potrzebujemy tekstu widocznego tylko dla czytników ekranu:
1// Styl CSS do ukrycia wizualnego, ale nie dla czytników ekranu
2const srOnlyStyle = {
3 position: 'absolute',
4 width: '1px',
5 height: '1px',
6 padding: 0,
7 margin: '-1px',
8 overflow: 'hidden',
9 clip: 'rect(0, 0, 0, 0)',
10 whiteSpace: 'nowrap',
11 borderWidth: 0,
12};
13
14function IconButton({ icon, label, onClick }) {
15 return (
16 <button onClick={onClick} aria-label={label}>
17 <span aria-hidden="true">{icon}</span>
18 <span style={srOnlyStyle}>{label}</span>
19 </button>
20 );
21}
22
23// Użycie
24<IconButton icon="▲" label="Rozpocznij misję" onClick={startMission} />Formularze dostępne
Formularze to jeden z najważniejszych obszarów dostępności. Każde pole musi mieć etykietę, a błędy muszą być jasno komunikowane:
1function CrewRegistrationForm() {
2 const [errors, setErrors] = useState({});
3
4 return (
5 <form aria-label="Rejestracja członka załogi">
6 <div>
7 <label htmlFor="crew-name">Imię i nazwisko:</label>
8 <input
9 id="crew-name"
10 type="text"
11 aria-required="true"
12 aria-invalid={!!errors.name}
13 aria-describedby={errors.name ? 'name-error' : undefined}
14 />
15 {errors.name && (
16 <span id="name-error" role="alert" style={{ color: 'red' }}>
17 {errors.name}
18 </span>
19 )}
20 </div>
21
22 <fieldset>
23 <legend>Specjalizacja:</legend>
24 <label>
25 <input type="radio" name="role" value="pilot" />
26 Pilot
27 </label>
28 <label>
29 <input type="radio" name="role" value="engineer" />
30 Inżynier
31 </label>
32 <label>
33 <input type="radio" name="role" value="scientist" />
34 Naukowiec
35 </label>
36 </fieldset>
37
38 <button type="submit">Dołącz do załogi</button>
39 </form>
40 );
41}Podsumowanie
Dostępność w React to nie dodatek, ale integralna część procesu tworzenia interfejsu:
- Semantyczny HTML to fundament - używaj
<nav>,<main>,<button>zamiast<div>na wszystko - Atrybuty ARIA uzupełniają semantykę, gdy natywne elementy nie wystarczają
- Zarządzanie fokusem jest krytyczne w SPA - modal musi "pułapkować" fokus, nawigacja musi być logiczna
- Nawigacja klawiaturą - każdy interaktywny element musi być dostępny bez myszy
- Live regions (
aria-live) ogłaszają dynamiczne zmiany treści - Formularze wymagają etykiet, opisów błędów i odpowiednich ról
Podobnie jak systemy stacji kosmicznej muszą być zaprojektowane tak, by każdy członek załogi mógł z nich korzystać w każdych warunkach - nawet gdy światła zgasną, a ekran dotykowy przestanie działać - tak dostępna aplikacja React zapewnia, że każdy użytkownik może skutecznie z niej korzystać.
Kod do tej lekcji: App.jsx
1import React, { useState, useRef, useEffect } from 'react';
2
3// ============================================
4// DEMO: Accessibility (a11y) w React
5// ============================================
6
7// --- Komponent 1: Nawigacja z ARIA ---
8function SpaceNavigation() {
9 return (
10 <nav aria-label="Menu glowne stacji kosmicznej">
11 <ul style={navListStyle}>
12 {[
13 { label: 'Panel kontroli', icon: '' },
14 { label: 'Mapa gwiazd', icon: '' },
15 { label: 'Zaloga', icon: '' },
16 { label: 'Systemy', icon: '' },
17 ].map(item => (
18 <li key={item.label}>
19 <button
20 style={navBtnStyle}
21 aria-label={item.label}
22 onClick={() => alert(item.label)}
23 >
24 <span aria-hidden="true">{item.icon}</span>
25 <span>{item.label}</span>
26 </button>
27 </li>
28 ))}
29 </ul>
30 </nav>
31 );
32}
33
34// --- Komponent 2: Modal z focus trap ---
35function AccessibleModal({ isOpen, onClose, title, children }) {
36 const modalRef = useRef(null);
37 const closeRef = useRef(null);
38
39 useEffect(() => {
40 if (isOpen && closeRef.current) {
41 closeRef.current.focus();
42 }
43 }, [isOpen]);
44
45 const handleKeyDown = (e) => {
46 if (e.key === 'Escape') onClose();
47
48 if (e.key === 'Tab' && modalRef.current) {
49 const focusable = modalRef.current.querySelectorAll(
50 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
51 );
52 const first = focusable[0];
53 const last = focusable[focusable.length - 1];
54
55 if (e.shiftKey && document.activeElement === first) {
56 e.preventDefault();
57 last.focus();
58 } else if (!e.shiftKey && document.activeElement === last) {
59 e.preventDefault();
60 first.focus();
61 }
62 }
63 };
64
65 if (!isOpen) return null;
66
67 return (
68 <div style={overlayStyle} onClick={onClose}>
69 <div
70 ref={modalRef}
71 role="dialog"
72 aria-modal="true"
73 aria-labelledby="modal-title"
74 style={modalStyle}
75 onClick={(e) => e.stopPropagation()}
76 onKeyDown={handleKeyDown}
77 >
78 <h2 id="modal-title" style={{ color: '#00d4ff', marginBottom: '12px' }}>
79 {title}
80 </h2>
81 {children}
82 <button ref={closeRef} onClick={onClose} style={closeBtnStyle}>
83 Zamknij (Esc)
84 </button>
85 </div>
86 </div>
87 );
88}
89
90// --- Komponent 3: Lista z nawigacja klawiaturowa ---
91function ShipSelector() {
92 const ships = [
93 { id: 1, name: 'Aurora', type: 'Krążownik', crew: 120 },
94 { id: 2, name: 'Nebula', type: 'Fregata', crew: 45 },
95 { id: 3, name: 'Vortex', type: 'Myśliwiec', crew: 2 },
96 { id: 4, name: 'Horizon', type: 'Transporter', crew: 300 },
97 { id: 5, name: 'Eclipse', type: 'Zwiadowca', crew: 8 },
98 ];
99
100 const [activeIndex, setActiveIndex] = useState(0);
101 const [selected, setSelected] = useState(null);
102
103 const handleKeyDown = (e) => {
104 switch (e.key) {
105 case 'ArrowDown':
106 e.preventDefault();
107 setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
108 break;
109 case 'ArrowUp':
110 e.preventDefault();
111 setActiveIndex(prev => Math.max(prev - 1, 0));
112 break;
113 case 'Enter':
114 case ' ':
115 e.preventDefault();
116 setSelected(ships[activeIndex]);
117 break;
118 }
119 };
120
121 return (
122 <div>
123 <h3 style={headingStyle}>Wybierz statek (nawiguj strzalkami)</h3>
124 <ul
125 role="listbox"
126 aria-label="Lista statkow kosmicznych"
127 tabIndex={0}
128 onKeyDown={handleKeyDown}
129 style={{ listStyle: 'none', padding: 0, outline: 'none' }}
130 >
131 {ships.map((ship, index) => (
132 <li
133 key={ship.id}
134 role="option"
135 aria-selected={index === activeIndex}
136 onClick={() => { setActiveIndex(index); setSelected(ship); }}
137 style={{
138 padding: '10px 14px',
139 background: index === activeIndex ? '#1a3a5c' : '#0d1b2a',
140 borderRadius: '6px',
141 marginBottom: '4px',
142 cursor: 'pointer',
143 border: index === activeIndex ? '1px solid #00d4ff' : '1px solid transparent',
144 display: 'flex',
145 justifyContent: 'space-between',
146 }}
147 >
148 <span>{ship.name} - {ship.type}</span>
149 <span style={{ opacity: 0.5 }}>Zaloga: {ship.crew}</span>
150 </li>
151 ))}
152 </ul>
153 {selected && (
154 <div aria-live="polite" style={selectedStyle}>
155 Wybrany statek: <strong>{selected.name}</strong> ({selected.type})
156 </div>
157 )}
158 </div>
159 );
160}
161
162// --- Komponent 4: Formularz z walidacja ---
163function CrewForm() {
164 const [name, setName] = useState('');
165 const [role, setRole] = useState('');
166 const [errors, setErrors] = useState({});
167 const [submitted, setSubmitted] = useState(false);
168
169 const validate = () => {
170 const newErrors = {};
171 if (!name.trim()) newErrors.name = 'Imie jest wymagane';
172 if (!role) newErrors.role = 'Wybierz specjalizacje';
173 setErrors(newErrors);
174 return Object.keys(newErrors).length === 0;
175 };
176
177 const handleSubmit = (e) => {
178 e.preventDefault();
179 if (validate()) {
180 setSubmitted(true);
181 setName('');
182 setRole('');
183 setTimeout(() => setSubmitted(false), 3000);
184 }
185 };
186
187 return (
188 <form onSubmit={handleSubmit} aria-label="Rejestracja czlonka zalogi">
189 <h3 style={headingStyle}>Rejestracja zalogi</h3>
190
191 <div style={{ marginBottom: '12px' }}>
192 <label htmlFor="crew-name" style={labelStyle}>Imie i nazwisko:</label>
193 <input
194 id="crew-name"
195 type="text"
196 value={name}
197 onChange={(e) => setName(e.target.value)}
198 aria-required="true"
199 aria-invalid={!!errors.name}
200 aria-describedby={errors.name ? 'name-error' : undefined}
201 style={inputStyle}
202 />
203 {errors.name && (
204 <span id="name-error" role="alert" style={errorStyle}>
205 {errors.name}
206 </span>
207 )}
208 </div>
209
210 <fieldset style={{ border: '1px solid #1a3a5c', borderRadius: '6px', padding: '12px', marginBottom: '12px' }}>
211 <legend style={{ color: '#00d4ff', padding: '0 8px' }}>Specjalizacja:</legend>
212 {['pilot', 'inzynier', 'naukowiec', 'medyk'].map(r => (
213 <label key={r} style={{ display: 'block', marginBottom: '6px', cursor: 'pointer' }}>
214 <input
215 type="radio"
216 name="role"
217 value={r}
218 checked={role === r}
219 onChange={(e) => setRole(e.target.value)}
220 style={{ marginRight: '8px' }}
221 />
222 {r.charAt(0).toUpperCase() + r.slice(1)}
223 </label>
224 ))}
225 {errors.role && (
226 <span role="alert" style={errorStyle}>{errors.role}</span>
227 )}
228 </fieldset>
229
230 <button type="submit" style={submitBtnStyle}>
231 Dolacz do zalogi
232 </button>
233
234 {submitted && (
235 <div aria-live="assertive" role="alert" style={successStyle}>
236 Rejestracja zakonczona pomyslnie!
237 </div>
238 )}
239 </form>
240 );
241}
242
243// --- Komponent 5: Live region ---
244function MissionStatus() {
245 const [status, setStatus] = useState('Oczekiwanie');
246 const statuses = ['Oczekiwanie', 'Odliczanie', 'Start!', 'W locie', 'Orbita osiagnieta'];
247
248 return (
249 <div>
250 <h3 style={headingStyle}>Status misji (aria-live)</h3>
251 <div aria-live="polite" aria-atomic="true" style={statusBoxStyle}>
252 Status: <strong style={{ color: '#ffd700' }}>{status}</strong>
253 </div>
254 <div style={{ display: 'flex', gap: '6px', marginTop: '8px', flexWrap: 'wrap' }}>
255 {statuses.map(s => (
256 <button
257 key={s}
258 onClick={() => setStatus(s)}
259 style={{
260 ...navBtnStyle,
261 background: status === s ? '#00d4ff' : '#1a2a3a',
262 color: status === s ? '#000' : '#fff',
263 }}
264 >
265 {s}
266 </button>
267 ))}
268 </div>
269 </div>
270 );
271}
272
273// --- Style ---
274const navListStyle = { listStyle: 'none', padding: 0, display: 'flex', gap: '6px', flexWrap: 'wrap' };
275const navBtnStyle = {
276 padding: '8px 14px', background: '#1a2a3a', border: 'none',
277 borderRadius: '6px', color: '#fff', cursor: 'pointer', fontSize: '13px',
278 display: 'flex', gap: '6px', alignItems: 'center',
279};
280const overlayStyle = {
281 position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
282 background: 'rgba(0,0,0,0.7)', display: 'flex',
283 alignItems: 'center', justifyContent: 'center', zIndex: 1000,
284};
285const modalStyle = {
286 background: '#0d1b2a', borderRadius: '12px', padding: '24px',
287 maxWidth: '400px', width: '90%', border: '1px solid #1a3a5c',
288};
289const closeBtnStyle = {
290 marginTop: '16px', padding: '8px 16px', background: '#f44336',
291 border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer',
292};
293const headingStyle = { color: '#00d4ff', marginBottom: '10px', fontSize: '15px' };
294const inputStyle = {
295 width: '100%', padding: '8px', background: '#0d1b2a',
296 border: '1px solid #1a3a5c', borderRadius: '6px', color: '#fff',
297 fontSize: '14px', boxSizing: 'border-box',
298};
299const labelStyle = { display: 'block', marginBottom: '4px', color: '#00d4ff', fontSize: '13px' };
300const errorStyle = { color: '#f44336', fontSize: '12px', display: 'block', marginTop: '4px' };
301const submitBtnStyle = {
302 padding: '10px 20px', background: '#4CAF50', border: 'none',
303 borderRadius: '6px', color: '#fff', cursor: 'pointer', fontSize: '14px',
304};
305const successStyle = {
306 marginTop: '12px', padding: '10px', background: '#1a3a1a',
307 borderRadius: '6px', color: '#4CAF50', border: '1px solid #4CAF50',
308};
309const selectedStyle = {
310 marginTop: '8px', padding: '10px', background: '#1a2a3a',
311 borderRadius: '6px', border: '1px solid #00d4ff',
312};
313const statusBoxStyle = {
314 padding: '12px', background: '#0d1b2a', borderRadius: '6px',
315 border: '1px solid #1a3a5c',
316};
317const sectionStyle = {
318 marginBottom: '24px', padding: '16px',
319 background: 'rgba(255,255,255,0.03)', borderRadius: '12px',
320 border: '1px solid rgba(255,255,255,0.08)',
321};
322
323function App() {
324 const [modalOpen, setModalOpen] = useState(false);
325
326 return (
327 <div style={{ padding: '20px', background: '#0a0e1a', color: '#e0e0e0', minHeight: '100vh', fontFamily: 'system-ui' }}>
328 <h1 style={{ color: '#ffd700', textAlign: 'center', marginBottom: '6px' }}>
329 Accessibility (a11y) w React
330 </h1>
331 <p style={{ textAlign: 'center', opacity: 0.6, marginBottom: '24px', fontSize: '13px' }}>
332 Testuj Tab, strzalki, Enter, Esc
333 </p>
334
335 <section style={sectionStyle}>
336 <h2 style={{ color: '#00d4ff', marginBottom: '10px' }}>Semantyczna nawigacja</h2>
337 <SpaceNavigation />
338 </section>
339
340 <section style={sectionStyle}>
341 <h2 style={{ color: '#00d4ff', marginBottom: '10px' }}>Modal z focus trap</h2>
342 <button onClick={() => setModalOpen(true)} style={navBtnStyle}>
343 Otworz modal misji
344 </button>
345 <AccessibleModal
346 isOpen={modalOpen}
347 onClose={() => setModalOpen(false)}
348 title="Szczegoly misji Alpha"
349 >
350 <p>Cel: Eksploracja systemu Kepler-442.</p>
351 <p>Czas trwania: 18 miesiecy.</p>
352 <input placeholder="Dodaj notatke..." style={{ ...inputStyle, marginTop: '8px' }} />
353 </AccessibleModal>
354 </section>
355
356 <section style={sectionStyle}>
357 <ShipSelector />
358 </section>
359
360 <section style={sectionStyle}>
361 <MissionStatus />
362 </section>
363
364 <section style={sectionStyle}>
365 <CrewForm />
366 </section>
367 </div>
368 );
369}
370
371export default App;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki jest główny cel wzorca Adapter w React?
2. Jak najczęściej implementujemy wzorzec Facade w React?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż atrybuty dostępności dla pola input z błędem walidacji:
- Edytor kodu
Wirtualizowana lista z optymalizacjami
- Klikanie w kolejności
Ułóż składnię React.forwardRef:
- Układanie w pionie
Uporządkuj implementację HOC:
- Edytor kodu
Zaawansowany Error Boundary