Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

useId - unikalne identyfikatory dla dostępności

Hook

useId
jest prostym, ale niezwykle ważnym narzędziem wprowadzonym w React 18. Generuje unikalne identyfikatory, które są stabilne zarówno po stronie serwera, jak i klienta, co jest kluczowe dla dostępności (accessibility) i poprawnego renderowania SSR.

Podstawy useId

1. Problem, który rozwiązuje useId

1// ❌ PROBLEM - niestabilne ID
2function BadExample() {
3  // Math.random() da różne wartości na serwerze i kliencie!
4  const id = Math.random().toString(36).substr(2, 9);
5  
6  return (
7    <div>
8      <label htmlFor={id}>Email:</label>
9      <input id={id} type="email" />
10    </div>
11  );
12}
13
14// ❌ PROBLEM - globalne liczniki
15let counter = 0;
16function AnotherBadExample() {
17  // Counter może być różny w różnych renderach
18  const id = `input-${counter++}`;
19  
20  return (
21    <div>
22      <label htmlFor={id}>Hasło:</label>
23      <input id={id} type="password" />
24    </div>
25  );
26}
27
28// ✅ ROZWIĄZANIE - useId
29import { useId } from 'react';
30
31function GoodExample() {
32  const id = useId();
33  
34  return (
35    <div>
36      <label htmlFor={id}>Imię:</label>
37      <input id={id} type="text" />
38    </div>
39  );
40}

2. Podstawowe użycie dla pojedynczych elementów

1import { useId } from 'react';
2
3function AccessibleInput({ label, type = 'text', ...props }) {
4  const id = useId();
5  
6  return (
7    <div style={{ marginBottom: '15px' }}>
8      <label 
9        htmlFor={id}
10        style={{ 
11          display: 'block', 
12          marginBottom: '5px',
13          fontWeight: 'bold'
14        }}
15      >
16        {label}
17      </label>
18      <input 
19        id={id} 
20        type={type}
21        style={{
22          width: '100%',
23          padding: '8px',
24          border: '1px solid #ddd',
25          borderRadius: '4px'
26        }}
27        {...props}
28      />
29    </div>
30  );
31}
32
33// Użycie komponentu
34function RegistrationForm() {
35  return (
36    <form style={{ maxWidth: '400px', margin: '0 auto' }}>
37      <h2>Rejestracja</h2>
38      <AccessibleInput label="Imię:" 
39      <AccessibleInput label="Nazwisko:" 
40      <AccessibleInput label="Email:" type="email" 
41      <AccessibleInput label="Hasło:" type="password" />
42      <button type="submit">Zarejestruj się</button>
43    </form>
44  );
45}

3. Użycie z wieloma powiązanymi elementami

1import { useId } from 'react';
2
3function RadioGroup({ label, options, name, onChange }) {
4  const baseId = useId();
5  
6  return (
7    <fieldset style={{ border: '1px solid #ddd', borderRadius: '8px', padding: '15px' }}>
8      <legend style={{ fontWeight: 'bold', padding: '0 10px' }}>{label}</legend>
9      {options.map((option, index) => {
10        const optionId = `${baseId}-${index}`;
11        
12        return (
13          <div key={option.value} style={{ marginBottom: '10px' }}>
14            <input
15              type="radio"
16              id={optionId}
17              name={name}
18              value={option.value}
19              onChange={onChange}
20              style={{ marginRight: '8px' }}
21            />
22            <label htmlFor={optionId} style={{ cursor: 'pointer' }}>
23              {option.label}
24            </label>
25          </div>
26        );
27      })}
28    </fieldset>
29  );
30}
31
32// Użycie
33function PreferencesForm() {
34  const handleThemeChange = (e) => console.log('Theme:', e.target.value);
35  const handleLanguageChange = (e) => console.log('Language:', e.target.value);
36  
37  return (
38    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
39      <h2>Preferencje</h2>
40      
41      <RadioGroup
42        label="Wybierz motyw:"
43        name="theme"
44        onChange={handleThemeChange}
45        options={[
46          { value: 'light', label: '☀️ Jasny' },
47          { value: 'dark', label: '🌙 Ciemny' },
48          { value: 'auto', label: '🔄 Automatyczny' }
49        ]}
50      />
51      
52      <div style={{ marginTop: '20px' }}>
53        <RadioGroup
54          label="Język interfejsu:"
55          name="language"
56          onChange={handleLanguageChange}
57          options={[
58            { value: 'pl', label: '🇵🇱 Polski' },
59            { value: 'en', label: '🇬🇧 English' },
60            { value: 'de', label: '🇩🇪 Deutsch' }
61          ]}
62        />
63      </div>
64    </div>
65  );
66}

Zaawansowane wzorce z useId

1. Komponenty z opisami ARIA

1import { useId } from 'react';
2
3function AccessibleTextField({ 
4  label, 
5  helperText, 
6  errorText, 
7  required = false,
8  ...props 
9}) {
10  const inputId = useId();
11  const helperId = useId();
12  const errorId = useId();
13  
14  const ariaDescribedBy = [
15    helperText && helperId,
16    errorText && errorId
17  ].filter(Boolean).join(' ');
18  
19  return (
20    <div style={{ marginBottom: '20px' }}>
21      <label 
22        htmlFor={inputId}
23        style={{ 
24          display: 'block', 
25          marginBottom: '5px',
26          fontWeight: 'bold'
27        }}
28      >
29        {label}
30        {required && <span style={{ color: 'red' }}> *</span>}
31      </label>
32      
33      <input
34        id={inputId}
35        aria-describedby={ariaDescribedBy || undefined}
36        aria-invalid={!!errorText}
37        aria-required={required}
38        style={{
39          width: '100%',
40          padding: '10px',
41          border: `2px solid ${errorText ? '#dc3545' : '#ddd'}`,
42          borderRadius: '4px',
43          fontSize: '16px'
44        }}
45        {...props}
46      />
47      
48      {helperText && !errorText && (
49        <div 
50          id={helperId}
51          style={{ 
52            marginTop: '5px', 
53            fontSize: '14px', 
54            color: '#666' 
55          }}
56        >
57          {helperText}
58        </div>
59      )}
60      
61      {errorText && (
62        <div 
63          id={errorId}
64          role="alert"
65          style={{ 
66            marginTop: '5px', 
67            fontSize: '14px', 
68            color: '#dc3545' 
69          }}
70        >
71          {errorText}
72        </div>
73      )}
74    </div>
75  );
76}
77
78// Przykład użycia z walidacją
79function ContactForm() {
80  const [email, setEmail] = useState('');
81  const [phone, setPhone] = useState('');
82  const [errors, setErrors] = useState({});
83  
84  const validateEmail = (value) => {
85    if (!value) return 'Email jest wymagany';
86    if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(value)) {
87      return 'Nieprawidłowy format email';
88    }
89    return '';
90  };
91  
92  const validatePhone = (value) => {
93    if (value && !/^d{9}$/.test(value.replace(/s/g, ''))) {
94      return 'Numer telefonu powinien mieć 9 cyfr';
95    }
96    return '';
97  };
98  
99  const handleEmailChange = (e) => {
100    const value = e.target.value;
101    setEmail(value);
102    setErrors(prev => ({ ...prev, email: validateEmail(value) }));
103  };
104  
105  const handlePhoneChange = (e) => {
106    const value = e.target.value;
107    setPhone(value);
108    setErrors(prev => ({ ...prev, phone: validatePhone(value) }));
109  };
110  
111  return (
112    <form style={{ maxWidth: '500px', margin: '0 auto' }}>
113      <h2>Formularz kontaktowy</h2>
114      
115      <AccessibleTextField
116        label="Adres email"
117        type="email"
118        value={email}
119        onChange={handleEmailChange}
120        helperText="Użyjemy tego adresu tylko do kontaktu w sprawie Twojego zapytania"
121        errorText={errors.email}
122        required
123      />
124      
125      <AccessibleTextField
126        label="Numer telefonu"
127        type="tel"
128        value={phone}
129        onChange={handlePhoneChange}
130        helperText="Opcjonalnie - przyspieszy kontakt"
131        errorText={errors.phone}
132      />
133      
134      <button 
135        type="submit"
136        disabled={!!errors.email || !!errors.phone}
137        style={{
138          padding: '10px 20px',
139          backgroundColor: '#007bff',
140          color: 'white',
141          border: 'none',
142          borderRadius: '4px',
143          cursor: 'pointer',
144          fontSize: '16px'
145        }}
146      >
147        Wyślij
148      </button>
149    </form>
150  );
151}

2. Komponenty typu accordion/collapse

1import { useId, useState } from 'react';
2
3function Accordion({ items }) {
4  const [openIndex, setOpenIndex] = useState(null);
5  
6  return (
7    <div style={{ border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}>
8      {items.map((item, index) => (
9        <AccordionItem
10          key={index}
11          title={item.title}
12          content={item.content}
13          isOpen={openIndex === index}
14          onToggle={() => setOpenIndex(openIndex === index ? null : index)}
15        />
16      ))}
17    </div>
18  );
19}
20
21function AccordionItem({ title, content, isOpen, onToggle }) {
22  const headerId = useId();
23  const panelId = useId();
24  
25  return (
26    <div style={{ borderBottom: '1px solid #ddd' }}>
27      <h3 style={{ margin: 0 }}>
28        <button
29          id={headerId}
30          aria-expanded={isOpen}
31          aria-controls={panelId}
32          onClick={onToggle}
33          style={{
34            width: '100%',
35            padding: '15px 20px',
36            border: 'none',
37            background: isOpen ? '#f8f9fa' : 'white',
38            textAlign: 'left',
39            fontSize: '16px',
40            cursor: 'pointer',
41            display: 'flex',
42            justifyContent: 'space-between',
43            alignItems: 'center'
44          }}
45        >
46          <span>{title}</span>
47          <span style={{ 
48            transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
49            transition: 'transform 0.3s'
50          }}>
5152          </span>
53        </button>
54      </h3>
55      
56      <div
57        id={panelId}
58        role="region"
59        aria-labelledby={headerId}
60        hidden={!isOpen}
61        style={{
62          padding: isOpen ? '20px' : '0 20px',
63          maxHeight: isOpen ? '500px' : '0',
64          overflow: 'hidden',
65          transition: 'all 0.3s ease-in-out',
66          backgroundColor: '#f8f9fa'
67        }}
68      >
69        {content}
70      </div>
71    </div>
72  );
73}
74
75// Użycie
76function FAQSection() {
77  const faqItems = [
78    {
79      title: 'Czym jest useId?',
80      content: 'useId to hook React, który generuje unikalne identyfikatory stabilne między serwerem a klientem. Jest szczególnie przydatny dla dostępności.'
81    },
82    {
83      title: 'Kiedy używać useId?',
84      content: 'Używaj useId zawsze, gdy potrzebujesz unikalnych ID dla elementów HTML, szczególnie dla powiązań label-input, ARIA attributes, czy innych relacji między elementami.'
85    },
86    {
87      title: 'Czy useId jest bezpieczny dla SSR?',
88      content: 'Tak! useId został specjalnie zaprojektowany, aby generować te same ID na serwerze i kliencie, eliminując problemy z hydratacją.'
89    }
90  ];
91  
92  return (
93    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
94      <h2>Często zadawane pytania</h2>
95      <Accordion items={faqItems} />
96    </div>
97  );
98}

3. Zaawansowane formularze z walidacją

1import { useId, useState } from 'react';
2
3function PasswordField({ 
4  label = "Hasło", 
5  value, 
6  onChange, 
7  showStrength = true 
8}) {
9  const [showPassword, setShowPassword] = useState(false);
10  const inputId = useId();
11  const strengthId = useId();
12  const requirementsId = useId();
13  
14  const calculateStrength = (password) => {
15    let strength = 0;
16    if (password.length >= 8) strength++;
17    if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
18    if (/d/.test(password)) strength++;
19    if (/[^a-zA-Z0-9]/.test(password)) strength++;
20    return strength;
21  };
22  
23  const strength = calculateStrength(value);
24  const strengthLabels = ['Bardzo słabe', 'Słabe', 'Średnie', 'Silne', 'Bardzo silne'];
25  const strengthColors = ['#dc3545', '#fd7e14', '#ffc107', '#28a745', '#20c997'];
26  
27  const requirements = [
28    { met: value.length >= 8, text: 'Minimum 8 znaków' },
29    { met: /[a-z]/.test(value) && /[A-Z]/.test(value), text: 'Małe i wielkie litery' },
30    { met: /d/.test(value), text: 'Przynajmniej jedna cyfra' },
31    { met: /[^a-zA-Z0-9]/.test(value), text: 'Przynajmniej jeden znak specjalny' }
32  ];
33  
34  return (
35    <div style={{ marginBottom: '20px' }}>
36      <label 
37        htmlFor={inputId}
38        style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
39      >
40        {label}
41      </label>
42      
43      <div style={{ position: 'relative' }}>
44        <input
45          id={inputId}
46          type={showPassword ? 'text' : 'password'}
47          value={value}
48          onChange={onChange}
49          aria-describedby={`${strengthId} ${requirementsId}`}
50          style={{
51            width: '100%',
52            padding: '10px 40px 10px 10px',
53            border: '2px solid #ddd',
54            borderRadius: '4px',
55            fontSize: '16px'
56          }}
57        />
58        
59        <button
60          type="button"
61          onClick={() => setShowPassword(!showPassword)}
62          aria-label={showPassword ? 'Ukryj hasło' : 'Pokaż hasło'}
63          style={{
64            position: 'absolute',
65            right: '10px',
66            top: '50%',
67            transform: 'translateY(-50%)',
68            background: 'none',
69            border: 'none',
70            cursor: 'pointer',
71            fontSize: '20px'
72          }}
73        >
74          {showPassword ? '👁️' : '👁️‍🗨️'}
75        </button>
76      </div>
77      
78      {showStrength && value && (
79        <>
80          <div 
81            id={strengthId}
82            style={{ marginTop: '10px' }}
83            role="status"
84            aria-live="polite"
85          >
86            <div style={{ 
87              display: 'flex', 
88              justifyContent: 'space-between',
89              marginBottom: '5px',
90              fontSize: '14px'
91            }}>
92              <span>Siła hasła:</span>
93              <span style={{ color: strengthColors[strength], fontWeight: 'bold' }}>
94                {strengthLabels[strength]}
95              </span>
96            </div>
97            
98            <div style={{ 
99              height: '4px', 
100              backgroundColor: '#e9ecef', 
101              borderRadius: '2px',
102              overflow: 'hidden'
103            }}>
104              <div style={{
105                width: `${(strength + 1) * 20}%`,
106                height: '100%',
107                backgroundColor: strengthColors[strength],
108                transition: 'all 0.3s ease'
109              }} />
110            </div>
111          </div>
112          
113          <ul 
114            id={requirementsId}
115            style={{ 
116              marginTop: '10px', 
117              paddingLeft: '20px',
118              fontSize: '14px'
119            }}
120          >
121            {requirements.map((req, index) => (
122              <li 
123                key={index}
124                style={{ 
125                  color: req.met ? '#28a745' : '#6c757d',
126                  listStyleType: req.met ? '✓' : '○'
127                }}
128              >
129                {req.text}
130              </li>
131            ))}
132          </ul>
133        </>
134      )}
135    </div>
136  );
137}
138
139// Kompletny formularz rejestracji
140function SecureRegistrationForm() {
141  const [formData, setFormData] = useState({
142    username: '',
143    email: '',
144    password: '',
145    confirmPassword: ''
146  });
147  
148  const [agreed, setAgreed] = useState(false);
149  const checkboxId = useId();
150  
151  const handleSubmit = (e) => {
152    e.preventDefault();
153    console.log('Form submitted:', formData);
154  };
155  
156  const passwordsMatch = formData.password === formData.confirmPassword;
157  
158  return (
159    <form 
160      onSubmit={handleSubmit}
161      style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}
162    >
163      <h2>Bezpieczna rejestracja</h2>
164      
165      <AccessibleTextField
166        label="Nazwa użytkownika"
167        value={formData.username}
168        onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
169        helperText="3-20 znaków, tylko litery i cyfry"
170        required
171      />
172      
173      <AccessibleTextField
174        label="Email"
175        type="email"
176        value={formData.email}
177        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
178        required
179      />
180      
181      <PasswordField
182        label="Hasło"
183        value={formData.password}
184        onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
185      />
186      
187      <PasswordField
188        label="Potwierdź hasło"
189        value={formData.confirmPassword}
190        onChange={(e) => setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))}
191        showStrength={false}
192      />
193      
194      {formData.confirmPassword && !passwordsMatch && (
195        <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '-15px', marginBottom: '15px' }}>
196          Hasła nie są identyczne
197        </div>
198      )}
199      
200      <div style={{ marginBottom: '20px' }}>
201        <input
202          type="checkbox"
203          id={checkboxId}
204          checked={agreed}
205          onChange={(e) => setAgreed(e.target.checked)}
206          style={{ marginRight: '8px' }}
207        />
208        <label htmlFor={checkboxId} style={{ fontSize: '14px' }}>
209          Akceptuję <a href="#">regulamin</a> i <a href="#">politykę prywatności</a>
210        </label>
211      </div>
212      
213      <button
214        type="submit"
215        disabled={!agreed || !passwordsMatch || !formData.password}
216        style={{
217          width: '100%',
218          padding: '12px',
219          backgroundColor: '#007bff',
220          color: 'white',
221          border: 'none',
222          borderRadius: '4px',
223          fontSize: '16px',
224          cursor: 'pointer',
225          opacity: (!agreed || !passwordsMatch || !formData.password) ? 0.6 : 1
226        }}
227      >
228        Zarejestruj się
229      </button>
230    </form>
231  );
232}

Praktyczne zastosowania useId

1. Komponenty wielokrotnego użytku

1import { useId } from 'react';
2
3// Uniwersalny komponent Toggle/Switch
4function Toggle({ label, checked, onChange, disabled = false }) {
5  const id = useId();
6  
7  return (
8    <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
9      <input
10        type="checkbox"
11        id={id}
12        checked={checked}
13        onChange={onChange}
14        disabled={disabled}
15        style={{ display: 'none' }}
16      />
17      <label
18        htmlFor={id}
19        style={{
20          position: 'relative',
21          width: '50px',
22          height: '24px',
23          backgroundColor: checked ? '#007bff' : '#ccc',
24          borderRadius: '12px',
25          cursor: disabled ? 'not-allowed' : 'pointer',
26          opacity: disabled ? 0.6 : 1,
27          transition: 'background-color 0.3s'
28        }}
29      >
30        <span
31          style={{
32            position: 'absolute',
33            top: '2px',
34            left: checked ? '26px' : '2px',
35            width: '20px',
36            height: '20px',
37            backgroundColor: 'white',
38            borderRadius: '50%',
39            transition: 'left 0.3s',
40            boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
41          }}
42        />
43      </label>
44      <span style={{ fontSize: '14px' }}>{label}</span>
45    </div>
46  );
47}
48
49// Komponent do oceny (rating)
50function StarRating({ label, value, onChange, max = 5 }) {
51  const baseId = useId();
52  
53  return (
54    <div>
55      <div style={{ marginBottom: '5px', fontWeight: 'bold' }}>
56        {label}
57      </div>
58      <div style={{ display: 'flex', gap: '5px' }}>
59        {Array.from({ length: max }, (_, i) => {
60          const ratingValue = i + 1;
61          const inputId = `${baseId}-${ratingValue}`;
62          
63          return (
64            <React.Fragment key={ratingValue}>
65              <input
66                type="radio"
67                id={inputId}
68                name={`rating-${baseId}`}
69                value={ratingValue}
70                checked={value === ratingValue}
71                onChange={() => onChange(ratingValue)}
72                style={{ display: 'none' }}
73              />
74              <label
75                htmlFor={inputId}
76                style={{
77                  fontSize: '24px',
78                  cursor: 'pointer',
79                  color: ratingValue <= value ? '#ffc107' : '#ddd'
80                }}
81              >
8283              </label>
84            </React.Fragment>
85          );
86        })}
87      </div>
88    </div>
89  );
90}

2. Komponenty z tooltips

1import { useId, useState } from 'react';
2
3function TooltipButton({ buttonText, tooltipText }) {
4  const [showTooltip, setShowTooltip] = useState(false);
5  const tooltipId = useId();
6  
7  return (
8    <div style={{ position: 'relative', display: 'inline-block' }}>
9      <button
10        aria-describedby={showTooltip ? tooltipId : undefined}
11        onMouseEnter={() => setShowTooltip(true)}
12        onMouseLeave={() => setShowTooltip(false)}
13        onFocus={() => setShowTooltip(true)}
14        onBlur={() => setShowTooltip(false)}
15        style={{
16          padding: '10px 20px',
17          backgroundColor: '#007bff',
18          color: 'white',
19          border: 'none',
20          borderRadius: '4px',
21          cursor: 'pointer'
22        }}
23      >
24        {buttonText}
25      </button>
26      
27      {showTooltip && (
28        <div
29          id={tooltipId}
30          role="tooltip"
31          style={{
32            position: 'absolute',
33            bottom: '100%',
34            left: '50%',
35            transform: 'translateX(-50%)',
36            marginBottom: '10px',
37            padding: '8px 12px',
38            backgroundColor: 'rgba(0,0,0,0.8)',
39            color: 'white',
40            borderRadius: '4px',
41            fontSize: '14px',
42            whiteSpace: 'nowrap',
43            zIndex: 1000
44          }}
45        >
46          {tooltipText}
47          <div style={{
48            position: 'absolute',
49            top: '100%',
50            left: '50%',
51            transform: 'translateX(-50%)',
52            width: 0,
53            height: 0,
54            borderLeft: '5px solid transparent',
55            borderRight: '5px solid transparent',
56            borderTop: '5px solid rgba(0,0,0,0.8)'
57          }} />
58        </div>
59      )}
60    </div>
61  );
62}

Najlepsze praktyki z useId

1. Kiedy używać useId

1// ✅ DOBRZE - elementy formularza
2function GoodForm() {
3  const emailId = useId();
4  return (
5    <>
6      <label htmlFor={emailId}>Email:</label>
7      <input id={emailId} type="email" />
8    </>
9  );
10}
11
12// ✅ DOBRZE - ARIA relationships
13function GoodAria() {
14  const descriptionId = useId();
15  return (
16    <>
17      <input aria-describedby={descriptionId} />
18      <span id={descriptionId}>Pomocny opis</span>
19    </>
20  );
21}
22
23// ❌ ŹLE - jako klucze w listach
24function BadList({ items }) {
25  return items.map(item => {
26    const id = useId(); // ❌ Nie używaj w pętlach!
27    return <li key={id}>{item}</li>;
28  });
29}
30
31// ❌ ŹLE - do identyfikacji danych
32function BadData() {
33  const user = {
34    id: useId(), // ❌ Nie do danych!
35    name: 'Jan'
36  };
37}

2. Prefixes dla lepszej organizacji

1import { useId } from 'react';
2
3function createIdGenerator(prefix) {
4  return function useIdWithPrefix() {
5    const id = useId();
6    return `${prefix}-${id}`;
7  };
8}
9
10// Użycie
11const useFormId = createIdGenerator('form');
12const useModalId = createIdGenerator('modal');
13const useTabId = createIdGenerator('tab');
14
15function MyForm() {
16  const emailId = useFormId();
17  const passwordId = useFormId();
18  
19  // Wygenerowane ID będą wyglądać jak: form-:r1:, form-:r2:
20}

3. Testowanie komponentów z useId

1// Komponenty używające useId są łatwe do testowania
2import { render, screen } from '@testing-library/react';
3import userEvent from '@testing-library/user-event';
4
5function LoginForm() {
6  const usernameId = useId();
7  const passwordId = useId();
8  
9  return (
10    <form>
11      <label htmlFor={usernameId}>Username:</label>
12      <input id={usernameId} type="text" />
13      
14      <label htmlFor={passwordId}>Password:</label>
15      <input id={passwordId} type="password" />
16      
17      <button type="submit">Login</button>
18    </form>
19  );
20}
21
22// Test
23test('form inputs are properly labeled', async () => {
24  render(<LoginForm />);
25  
26  // Możemy znaleźć inputy przez ich labels
27  const usernameInput = screen.getByLabelText('Username:');
28  const passwordInput = screen.getByLabelText('Password:');
29  
30  await userEvent.type(usernameInput, 'testuser');
31  await userEvent.type(passwordInput, 'password123');
32  
33  expect(usernameInput).toHaveValue('testuser');
34  expect(passwordInput).toHaveValue('password123');
35});

useId
to prosty, ale potężny hook, który znacząco poprawia dostępność aplikacji React. Używaj go wszędzie tam, gdzie potrzebujesz stabilnych, unikalnych identyfikatorów dla elementów HTML.

Ir a CodeWorlds