Kurs JavaScript i React · Moduł 8: Zaawansowane wzorce React

Accessibility (a11y) w React

6 min czytania
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:

AtrybutZastosowanie
roleDefiniuje rolę elementu (alert, dialog, tabpanel)
aria-labelTekstowa etykieta dla elementu
aria-labelledbyID elementu, który jest etykietą
aria-describedbyID elementu z dodatkowym opisem
aria-liveOgłasza dynamiczne zmiany treści
aria-expandedCzy element jest rozwinięty
aria-hiddenUkrywa element przed czytnikiem
aria-disabledWskazuje 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. 1. Jaki jest główny cel wzorca Adapter w React?

  2. 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

Przydatne artykuły