Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Nega oddiy yechimlar yaramaydi
- 2.1 Prop bilan id berish
- 2.2 Hisoblagich
- 3. useId
- 3.1 Sintaksis
- 3.2 Qayta ishlatiladigan Field
- 3.3 useId qanday "biladi"?
- 4. Bitta id — bir nechta bog'lanish
- 4.1 Izoh va xato matni
- 4.2 Radio tugmalar guruhi
- 5. Id formati va identifierPrefix
- 5.1 Formatga tayanmang
- 5.2 Bir sahifada bir nechta React ilova
- 6. Server va hydration
- 7. key uchun emas
- 8. Vazifalar qadami: aria-labelledby id'lari
- 8.1 Nima o'zgaradi
- 8.2 O'zgarishlar
- 8.3 Nega hamma id'lar emas
- 8.4 Natija
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): izohli maydon
- 2-mashq (o'rta): uchta xato
- 3-mashq (qiyin): to'lov usuli
- 11. Real ishda
- Xulosa
- Manbalar
React useId: label va aria atributlari uchun barqaror, noyob id
Qisqacha:
useId()komponentning har bir nusxasiga noyob va barqaror id beradi:const id = useId()→<label htmlFor={id}>va<input id={id}>. Komponent sahifada ikki marta ishlatilsa ham id'lar to'qnashmaydi, qayta chizishda o'zgarmaydi va serverda ham brauzerdagidek chiqadi. BittauseIddan bir nechta id yasash mumkin:${id}-izoh,${id}-xato. Ro'yxatdagikeyuchun esauseIdishlatilmaydi —keyma'lumotdan keladi.
Bu darsda
- Qo'lda yozilgan va hisoblagich bilan yasalgan id nega React'da buzilishini ko'rsata olasiz.
useIdbilanlabel,input,aria-describedbyni bog'laysiz.- Bitta id'dan bir nechta bog'langan id yasaysiz.
identifierPrefixva id formati haqida bilasiz, unga tayanmaslikni tushunasiz.vazifalardagiaria-labelledbyid'lariniuseIdga o'tkazasiz.
Oldin bilishingiz kerak: Formalar I: controlled inputlar, label: har maydonning nomi, Qulay va accessible forma, Hooklar qoidalari.
1. Nega bu kerak?
«Bahor» bron formasida telefon maydoni bor. Sardor uni qayta ishlatiladigan komponentga chiqardi: Field — yorliq va maydon. Yorliq maydonga htmlFor="telefon" bilan bog'langan (label darsidagidek). Keyin Jasur aka so'radi: "Zaxira telefon ham bo'lsin". Sardor Field ni ikkinchi marta qo'ydi.
Natija — sahifada ikkita id="telefon". HTML qoidasi buzildi: id hujjatda noyob bo'lishi kerak (Global atributlar). Tekshiramiz:
import { useEffect } from "react";
function Field({ label, name }) {
return (
<p>
<label htmlFor="telefon">{label}</label>{" "}
<input id="telefon" name={name} />
</p>
);
}
function App() {
useEffect(() => {
const backup = document.querySelectorAll("label")[1];
const count = document.querySelectorAll("#telefon").length;
console.log("id'lar:", count);
console.log("zaxira yorlig'i ->", backup.control.name);
}, []);
return (
<form>
<Field label="Telefon" name="asosiy" />
<Field label="Zaxira telefon" name="zaxira" />
</form>
);
}Konsolda:
id'lar: 2
zaxira yorlig'i -> asosiyEkranda:
Telefon
Zaxira telefonlabel.control — yorliq bog'langan maydon. «Zaxira telefon» yorlig'i birinchi maydonga ishora qilyapti: brauzer birinchi topilgan id ni oladi. Foydalanuvchi «Zaxira telefon» ni bossa, kursor yuqoridagi maydonga tushadi. Malika esa ekran o'quvchisida ikkinchi maydonga kelganda uning nomini umuman eshitmaydi.
Hayotiy o'xshatish: restorandagi stol raqamlari. Ikkala zalda ham "5-stol" bo'lsa, ofitsiant osh'ni noto'g'ri stolga olib boradi. Har stolga butun bino bo'yicha noyob raqam kerak. useId — shunday raqam beruvchi.
2. Nega oddiy yechimlar yaramaydi
2.1 Prop bilan id berish
Birinchi fikr — id'ni otadan prop qilib berish: <Field id="telefon-1" />. Bu ishlaydi, lekin har safar noyob nomni o'ylab topish sizning zimmangizda. Komponent kutubxonaga chiqsa yoki bir sahifada o'nta forma bo'lsa, to'qnashuv vaqt masalasi.
2.2 Hisoblagich
Vanilla JavaScript'da shunday qilardik:
let counter = 0;
function createField(label) {
const id = `maydon-${counter++}`;
return `<label for="${id}">${label}</label><input id="${id}">`;
}
console.log(createField("Ism"));
console.log(createField("Telefon"));Konsolda:
<label for="maydon-0">Ism</label><input id="maydon-0">
<label for="maydon-1">Telefon</label><input id="maydon-1">Vanilla'da bu to'g'ri: createField har maydon uchun bir marta chaqiriladi. React'da esa komponent funksiyasi ko'p marta chaqiriladi — har qayta chizishda (Pure komponentlar). Hisoblagichni render ichiga qo'yamiz:
import { useState } from "react";
let counter = 0;
function App() {
const [n, setN] = useState(0);
const id = `maydon-${counter++}`; // ❌ har renderda yangi
console.log("render", n, id);
return (
<p>
<label htmlFor={id}>Ism</label>{" "}
<input id={id} />{" "}
<button onClick={() => setN(n + 1)}>Qayta chizish</button>
</p>
);
}Konsolda:
render 0 maydon-0
render 1 maydon-1Ekranda:
Ism Qayta chizishHar bosishda id o'zgaradi. Yorliq va maydon bitta renderda yasalgani uchun hozircha bir-biriga mos, lekin:
- id'ga tayangan boshqa kod (
aria-describedby, CSS, test) har renderda "yo'qotib qo'yadi"; - StrictMode dev'da komponentni ikki marta chaqiradi (StrictMode) — id'lar sakrab ketadi;
- serverda tayyorlangan sahifada esa umuman buziladi (pastda).
StrictMode bilan ko'ramiz:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
let counter = 0;
function Field({ label }) {
const id = `maydon-${counter++}`;
console.log(label, id);
return (
<p>
<label htmlFor={id}>{label}</label>
<input id={id} />
</p>
);
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<Field label="Ism" />
<Field label="Telefon" />
</StrictMode>,
);
setTimeout(() => {
const inputs = [...document.querySelectorAll("input")];
console.log(inputs.map((i) => i.id));
}, 50);Konsolda:
Ism maydon-0
Ism maydon-1
Telefon maydon-2
Telefon maydon-3
(2) ['maydon-1', 'maydon-3']Ekranda:
Ism
TelefonIkki maydon uchun to'rtta id sarflandi, DOM'ga ikkinchi va to'rtinchisi tushdi. Render "toza" bo'lishi kerak edi — modul o'zgaruvchisini o'zgartirish esa yon ta'sir.
Tekshirib ko'ring:
const id = useRef("maydon-" + counter++)deb yozsak-chi?useRefqiymati renderlar orasida saqlanadi (useRef I).
Javob
Qisman yordam beradi: keyingi renderlarda id o'zgarmaydi. Lekin useRef("maydon-" + counter++) dagi ifoda baribir har renderda hisoblanadi — counter o'saveradi, faqat natija tashlab yuboriladi. StrictMode'dagi sakrash va serverdagi muammo qoladi: server va brauzer hisoblagichlari har xil sondan boshlanadi.
3. useId
3.1 Sintaksis
import { useId, useState } from "react";
function App() {
const [n, setN] = useState(0);
const id = useId();
console.log("render", n, id);
return (
<p>
<label htmlFor={id}>Ism</label>{" "}
<input id={id} />{" "}
<button onClick={() => setN(n + 1)}>Qayta chizish</button>
</p>
);
}Konsolda:
render 0 _r_0_
render 1 _r_0_Ekranda:
Ism Qayta chizishuseId() argumentsiz chaqiriladi va satr qaytaradi. Qayta chizishda — o'sha satr. Komponentning har bir nusxasi o'z id'sini oladi:
| Qadam | Nima bo'ldi | id |
|---|---|---|
| 1-render | useId() birinchi marta |
_r_0_ (yangi) |
| «Qayta chizish» | state o'zgardi, qayta render | _r_0_ (o'sha) |
| komponent olib tashlanib, qayta qo'yilsa | yangi nusxa | yangi id |
3.2 Qayta ishlatiladigan Field
import { useEffect, useId } from "react";
function Field({ label, name }) {
const id = useId();
return (
<p>
<label htmlFor={id}>{label}</label>{" "}
<input id={id} name={name} />
</p>
);
}
function App() {
useEffect(() => {
const backup = document.querySelectorAll("label")[1];
console.log("zaxira yorlig'i ->", backup.control.name);
}, []);
return (
<form>
<Field label="Telefon" name="asosiy" />
<Field label="Zaxira telefon" name="zaxira" />
</form>
);
}Konsolda:
zaxira yorlig'i -> zaxiraEkranda:
Telefon
Zaxira telefonEndi har yorliq o'z maydoniga bog'langan. Field ni o'n marta qo'ysangiz ham to'qnashuv bo'lmaydi va otaga hech qanday nom o'ylab topish kerak emas.
3.3 useId qanday "biladi"?
React id'ni chaqiruvlar sonidan emas, komponentning daraxtdagi o'rnidan yasaydi: "ildizning 1-bolasi ichidagi 2-bola" kabi yo'l. Shuning uchun:
- qayta renderda o'rin o'sha — id ham o'sha;
- StrictMode'dagi ikkinchi chaqiruvda ham o'rin o'sha — id ham o'sha;
- serverda ham, brauzerda ham daraxt bir xil bo'lsa — id bir xil.
Bu — hooklar qoidasiga tayangan yechim (Hooklar qoidalari): hooklar har renderda bir xil tartibda chaqirilgani uchun React har useId chaqiruvini taniydi.
flowchart TD
F["form"] --> A["Field: Telefon"]
F --> B["Field: Zaxira telefon"]
A --> A1["useId: 1-o'rin → id A"]
B --> B1["useId: 2-o'rin → id B"]
A1 --> S["serverda ham, brauzerda ham<br/>qayta renderda ham — o'sha id"]
B1 --> SDiagramma g'oyani ko'rsatadi: id hisoblagichdan emas, komponentning daraxtdagi manzilidan kelib chiqadi. Manzil o'zgarmasa — id ham o'zgarmaydi.
4. Bitta id — bir nechta bog'lanish
4.1 Izoh va xato matni
Yaxshi forma maydonida yorliqdan tashqari izoh ("+998 bilan yozing") va xato matni ham bo'ladi (Qulay va accessible forma). Ekran o'quvchi ularni maydon bilan birga o'qishi uchun aria-describedby kerak. Uning qiymati — izoh elementining id'si. Har element uchun alohida useId chaqirish shart emas — bitta id'ga qo'shimcha qo'shiladi:
import { useId, useState } from "react";
function PhoneField() {
const id = useId();
const hintId = `${id}-izoh`;
const errorId = `${id}-xato`;
const [value, setValue] = useState("");
const [error, setError] = useState(null);
function check() {
const ok = value.startsWith("+998");
setError(ok ? null : "Raqam +998 bilan boshlansin");
}
return (
<div>
<label htmlFor={id}>Telefon</label>{" "}
<input
id={id}
value={value}
onChange={(e) => setValue(e.target.value)}
aria-describedby={error ? `${hintId} ${errorId}` : hintId}
aria-invalid={error !== null}
/>
<p id={hintId}>Masalan: +998 90 000 00 00</p>
{error && <p id={errorId} role="alert">{error}</p>}
<button onClick={check}>Tekshirish</button>
</div>
);
}
export default PhoneField;Ekranda:
Telefon
Masalan: +998 90 000 00 00
Raqam +998 bilan boshlansin
TekshirishMalika maydonga kelganda ekran o'quvchi shunday o'qiydi: "Telefon, tahrirlanadigan maydon, noto'g'ri. Masalan: +998 90 000 00 00. Raqam +998 bilan boshlansin." aria-describedby bir nechta id'ni bo'sh joy bilan oladi. aria-invalid — "qiymat noto'g'ri" belgisi (ARIA asoslari).
Ikkita PhoneField qo'ysangiz, ularning -izoh va -xato id'lari ham har xil bo'ladi — hammasi bitta noyob asosdan.
Tekshirib ko'ring: Nega
useId()ni uch marta chaqirmay, bitta id'ga qo'shimcha qo'shdik? Uch marta chaqirish xatomi?
Javob
Xato emas — uchta useId() ham ishlaydi va noyob bo'ladi. Lekin bitta asos o'qishga qulay: DevTools'da _r_0_, _r_0_-izoh, _r_0_-xato ni ko'rib, ular bitta maydonga tegishli ekanini darhol tushunasiz. react.dev ham shu usulni tavsiya qiladi.
4.2 Radio tugmalar guruhi
useId faqat id uchun emas. Radio tugmalar guruhi umumiy name ga ega bo'lishi kerak (formalar). Sahifada ikki PaymentChoice bo'lsa, name="tolov" to'qnashadi — biri tanlansa, ikkinchisidagi belgi o'chadi. Noyob name ham useId dan olinadi (3-mashqda yozasiz).
5. Id formati va identifierPrefix
5.1 Formatga tayanmang
React 19.3.0 da id'lar _r_0_, _r_1_ ko'rinishida. Serverda tayyorlangan sahifada — _R_0_ (katta R). React 18 da esa :r0: edi. Format versiyalar orasida o'zgaradi va hujjatlanmagan — kodingiz unga bog'lanmasin:
- id'ni CSS'da ishlatmang (
#_r_0_ { … }) — u o'zgaradi. CSS uchunclassName. document.querySelectorbilan id bo'yicha qidirmang — React'da elementnirefbilan oling (useRef II).- Testda
getByLabelText("Telefon")kabi foydalanuvchi ko'radigan narsaga tayaning, id'ga emas.
5.2 Bir sahifada bir nechta React ilova
Ba'zan bitta sahifada ikkita alohida React ildizi bo'ladi: masalan, sayt — bitta ilova, bron vidjeti — boshqasi. Ikkalasi ham _r_0_ dan boshlasa — to'qnashuv. Buning uchun ildizga prefiks beriladi:
import { useId } from "react";
import { createRoot } from "react-dom/client";
function Field({ label }) {
const id = useId();
console.log(label, id);
return (
<p>
<label htmlFor={id}>{label}</label> <input id={id} />
</p>
);
}
const root = createRoot(document.getElementById("root"), {
identifierPrefix: "bron-",
});
root.render(<Field label="Mehmonlar soni" />);Konsolda:
Mehmonlar soni _bron-r_0_Ekranda:
Mehmonlar soniidentifierPrefix — createRoot ning sozlamasi; serverda (renderToString) ham xuddi shu nom bilan beriladi va ikki tomonda bir xil bo'lishi shart.
6. Server va hydration
Hozircha vazifalar sahifasini brauzer chizadi. Lekin ko'p saytlarda HTML serverda tayyorlanadi, brauzerda esa React uni jonlantiradi — bu hydration ("suv berish", tayyor HTML'ga hodisa va holatni ulash) deb ataladi. Uni keyingi qismlarda to'liq o'rganamiz. Bugun bitta narsani ko'ramiz: hydration paytida React serverdan kelgan HTML'ni brauzerdagi render bilan solishtiradi.
Hisoblagichli Field: server ko'p so'rovga xizmat qilgan va hisoblagich 57 ga yetgan. Brauzerdagi hisoblagich esa 0 dan boshlanadi. Konsolda uzun xabar chiqadi — undan sizga ikki joy kerak: birinchi qator (nima bo'ldi) va oxiridagi +/- qatorlar (qaysi atribut farq qildi). O'rtadagi ro'yxat — React taxmin qilgan sabablar:
import { hydrateRoot } from "react-dom/client";
let counter = 0;
function Field({ label }) {
const id = `maydon-${counter++}`;
return (
<p>
<label htmlFor={id}>{label}</label> <input id={id} />
</p>
);
}
const root = document.getElementById("root");
// serverdan kelgan HTML (u yerda hisoblagich 57 da edi)
root.innerHTML =
'<p><label for="maydon-57">Ism</label> <input id="maydon-57"></p>';
hydrateRoot(root, <Field label="Ism" />);Konsolda:
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Field label="Ism">
<p>
<label
+ htmlFor="maydon-0"
- htmlFor="maydon-57"
>
+ Ism
<input
+ id="maydon-0"
- id="maydon-57"
>Ekranda:
IsmTarjima: "Daraxt jonlantirildi, lekin server chizgan HTML'ning ba'zi atributlari brauzerdagi qiymatlarga mos kelmadi. Bu tuzatilmaydi." Ro'yxatda sabablar: Date.now(), Math.random() kabi har chaqiruvda o'zgaradigan qiymat — bizning hisoblagich ham shunday. Pastdagi + — brauzer kutgan, - — serverda kelgan qiymat. "Tuzatilmaydi" degani: DOM'da maydon-57 qoladi, React esa maydon-0 deb hisoblaydi.
Endi useId bilan. Server HTML'ini haqiqiy react-dom/server renderToString dan oldik — u _R_1_ va _R_2_ id'larini berdi:
import { useId } from "react";
import { hydrateRoot } from "react-dom/client";
function Field({ label }) {
const id = useId();
return (
<p>
<label htmlFor={id}>{label}</label> <input id={id} />
</p>
);
}
const root = document.getElementById("root");
// react-dom/server renderToString natijasi (React 19.3.0)
root.innerHTML =
'<form><p><label for="_R_1_">Ism</label> <input id="_R_1_"/></p>' +
'<p><label for="_R_2_">Telefon</label> ' +
'<input id="_R_2_"/></p></form>';
hydrateRoot(
root,
<form>
<Field label="Ism" />
<Field label="Telefon" />
</form>,
);
setTimeout(() => {
console.log([...root.querySelectorAll("input")].map((i) => i.id));
}, 50);Konsolda:
(2) ['_R_1_', '_R_2_']Ekranda:
Ism
TelefonOgohlantirish yo'q: brauzerdagi useId ham daraxtdagi o'sha o'rinlardan o'sha id'larni yasadi. useId ning asosiy sababi aynan shu — u serverda va brauzerda bir xil natija beradigan qilib loyihalangan.
7. key uchun emas
Ro'yxat key i haqida Ro'yxatlar va key darsida aytilgan qoida: key ma'lumotdan keladi (taom.id), chunki u "qaysi qator qaysi ma'lumot" ekanini bildiradi. useId esa "komponentning qaysi nusxasi"ni bildiradi — boshqa narsa.
Sardor shunday yozib ko'rdi:
// ❌ useId callback ichida va key sifatida
{items.map((t) => <li key={useId()}>{t}</li>)}Ekranda xato chiqmasligi mumkin, lekin linter (eslint-plugin-react-hooks 7.1.1) darhol ushlaydi:
6:34 error React Hook "useId" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks"useId hookini callback ichida chaqirib bo'lmaydi. Hooklar React komponenti yoki custom hook ichida chaqirilishi kerak." .map() ga berilgan funksiya — callback; ro'yxat uzunligi o'zgarsa, hooklar soni o'zgaradi va Hooklar qoidalari buziladi. Ikkinchi xato — ma'noda: qator o'chirilsa, keyingi qatorlarning "o'rni" o'zgaradi va ular boshqa id oladi. React ularni yangi qatorlar deb o'ylab, state'ini tashlab yuboradi.
Xuddi shunday, useId — ma'lumot id'si emas. Yangi vazifaning id si serverdan yoki keyingiId() dan keladi (State III), useId dan emas: u sahifa yangilanganda boshqacha bo'lishi mumkin.
| Vazifa | Nima ishlatiladi |
|---|---|
label ↔ input, aria-* bog'lash |
useId() |
radio guruhning name i |
useId() |
ro'yxat key i |
ma'lumotning id'si (taom.id) |
| yangi yozuvning id'si | server yoki crypto.randomUUID() |
| CSS selektori | className |
8. Vazifalar qadami: aria-labelledby id'lari
8.1 Nima o'zgaradi
vazifalar da uchta joyda qo'lda yozilgan id bor. Ular sarlavhani bo'limga bog'laydi (aria-labelledby — "bu bo'limning nomi o'sha elementda"):
App.tsx:<h2 id="royxat-sarlavha">va<ul aria-labelledby="royxat-sarlavha">;Import.tsx:<h2 id="import-sarlavha">va<section aria-labelledby="import-sarlavha">;OchirishDialog.tsx:<h2 id="ochirish-sarlavha">va<dialog aria-labelledby="ochirish-sarlavha">.
Hozir har biri sahifada bitta — to'qnashuv yo'q. Lekin Import ni ikkinchi marta qo'ysak (masalan, "fayldan" va "matndan" ikki zona), muammo qaytadi. Yangi branch: refactor/useid.
8.2 O'zgarishlar
OchirishDialog.tsx:
+import { useId } from "react";
import type { Ref } from "react";
…
export function OchirishDialog({ ref, matn, onJavob }: Props) {
+ // Dialog nomi — sarlavhasi (aria-labelledby);
+ // id — useId (17/#37). "ochirish-matn" qoladi:
+ // e2e ssenariy uni shu nom bilan o'qiydi
+ const sarlavhaId = useId();
return (
<dialog
ref={ref}
id="ochirish-dialog"
- aria-labelledby="ochirish-sarlavha"
+ aria-labelledby={sarlavhaId}
aria-describedby="ochirish-matn"
…
- <h2 id="ochirish-sarlavha">Vazifa o'chirilsinmi?</h2>
+ <h2 id={sarlavhaId}>Vazifa o'chirilsinmi?</h2>Import.tsx va App.tsx da ham xuddi shunday: const sarlavhaId = useId();, ikki joyga {sarlavhaId}. App.tsx dagi izoh (telefonga sig'ishi uchun qatorlari qayta bo'lingan, matni o'sha):
// useId (17/#37): sarlavha va ro'yxatni bog'lovchi
// (aria-labelledby) noyob id — qo'lda o'ylab topilgan
// "royxat-sarlavha" o'rniga. Komponent ikki marta chizilsa
// ham to'qnashmaydi; key uchun ishlatilmaydi
const sarlavhaId = useId();8.3 Nega hamma id'lar emas
aria-describedby="ochirish-matn" o'zgarmadi. Sabab — brauzer tekshiruvi (e2e ssenariy): u #ochirish-matn, #yangi-matn-xato, #tema-*, #vazifa-N kabi id'lar orqali elementlarni topadi. Bular ilovaning "tashqi shartnomasi": 13–16-qismda yozilgan 159 ta tekshiruv ularga tayanadi. useId faqat hech kim tashqaridan qidirmaydigan, faqat ichki bog'lanish uchun kerak bo'lgan id'larga qo'llandi. Qoida: tashqaridan qidiriladigan id barqaror, qo'lda tanlangan nomga muhtoj; faqat ichki bog'lanish uchun kerak bo'lgan id — useId dan.
8.4 Natija
useId nima berishini Vitest'da vaqtinchalik test bilan ko'rdik — ikkita <Import /> chizildi va har biridagi ikki id o'qildi. Kutilgan qiymatni ataylab bo'sh massiv qilib yozdik, shunda haqiqiy qiymat "Received" da chiqadi:
- Expected
+ Received
- []
+ [
+ [
+ "_r_0_",
+ "_r_0_",
+ ],
+ [
+ "_r_1_",
+ "_r_1_",
+ ],
+ ]Har juftlik — bitta Import ning section dagi aria-labelledby va h2 dagi id: ular teng. Ikki nusxa esa har xil id oldi. Bu test kanonga kirmadi — u faqat ko'rish uchun edi.
Brauzer tekshiruvidagi tegishli qator (159 tadan biri):
✅ dialog: nomi (aria-labelledby): "Vazifa o'chirilsinmi?"Ekran o'quvchi dialogni hamon "Vazifa o'chirilsinmi?" deb e'lon qiladi. Testlar: node 148, Vitest 23 — o'zgarishsiz. Brauzer: 159/159. Diff: 3 fayl, +24 −8.
Commit:
refactor: aria-labelledby id'lari useId bilan9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| ikkinchi yorliq birinchi maydonga fokus beradi | qo'lda yozilgan id ikki marta | useId() |
| id har renderda o'zgaradi | hisoblagich render ichida | useId() |
A tree hydrated but some attributes … didn't match |
server va brauzer id'lari har xil | useId(), server va brauzerda bir xil daraxt |
React Hook "useId" cannot be called inside a callback |
.map() ichida useId |
key — ma'lumotdan |
| CSS ishlamay qoldi | #_r_0_ selektori |
className |
| ikki React ildizida bir xil id | ikkalasi ham _r_0_ dan |
identifierPrefix |
10. Mashqlar
Mashqlar kurs/mashqlar/17/37-useid/ da.
1-mashq (oson): izohli maydon
Bo'sh joylarni to'ldiring: maydon yorliq bilan va izoh bilan bog'lansin.
function GuestsField() {
const id = [___:useId]();
return (
<p>
<label htmlFor={}>Mehmonlar soni</label>
<input id={id} aria-describedby={`${id}-izoh`} />
<small id={}>1 dan 12 gacha</small>
</p>
);
}Yechim
import { useEffect, useId } from "react";
function GuestsField() {
const id = useId();
return (
<p>
<label htmlFor={id}>Mehmonlar soni</label>{" "}
<input id={id} aria-describedby={`${id}-izoh`} />{" "}
<small id={`${id}-izoh`}>1 dan 12 gacha</small>
</p>
);
}
function App() {
useEffect(() => {
const input = document.querySelector("input");
const hint = document.getElementById(
input.getAttribute("aria-describedby"),
);
console.log(input.labels[0].textContent, "->", hint.textContent);
}, []);
return <GuestsField />;
}Konsolda:
Mehmonlar soni -> 1 dan 12 gachaEkranda:
Mehmonlar soni 1 dan 12 gachainput.labels — maydonga bog'langan yorliqlar. Effektdagi getElementById — faqat tekshirish uchun; ilova kodida bunday qidiruv kerak emas.
2-mashq (o'rta): uchta xato
Sardorning bron formasida uchta muammo bor. Toping va tuzating: har maydon o'z yorlig'iga bog'lansin, id'lar qayta chizishda o'zgarmasin, ro'yxat key i to'g'ri bo'lsin. Konsol toza bo'lsin.
import { useState } from "react";
let counter = 0;
const dishes = [
{ id: 11, name: "Osh" },
{ id: 12, name: "Manti" },
];
function Field({ label }) {
const id = `f-${counter++}`;
return (
<p>
<label htmlFor="mehmon">{label}</label> <input id={id} />
</p>
);
}
function App() {
const [note] = useState("");
return (
<form>
<Field label="Ism" />
<Field label="Telefon" />
<ul>
{dishes.map((d, i) => <li key={i}>{d.name}</li>)}
</ul>
<p>{note}</p>
</form>
);
}Ishora: «Bitta id — bir nechta bog'lanish» va «key uchun emas» bo'limlari.
Yechim
import { useEffect, useId, useState } from "react";
const dishes = [
{ id: 11, name: "Osh" },
{ id: 12, name: "Manti" },
];
function Field({ label }) {
const id = useId();
return (
<p>
<label htmlFor={id}>{label}</label> <input id={id} />
</p>
);
}
function App() {
const [note] = useState("");
useEffect(() => {
const ok = [...document.querySelectorAll("label")]
.every((l) => l.control !== null);
console.log("hamma yorliq bog'langan:", ok);
}, []);
return (
<form>
<Field label="Ism" />
<Field label="Telefon" />
<ul>
{dishes.map((d) => <li key={d.id}>{d.name}</li>)}
</ul>
<p>{note}</p>
</form>
);
}Konsolda:
hamma yorliq bog'langan: trueEkranda:
Ism
Telefon
Osh
MantihtmlFor="mehmon"— yorliq hech qaysi maydonga bog'lanmagan edi (bunday id yo'q). EndihtmlFor={id}.- Hisoblagich har renderda yangi id berardi —
useId(). key={i}— indeks; taomlar tartibi o'zgarsa, React qatorlarni adashtiradi.key={d.id}— ma'lumotdan.
3-mashq (qiyin): to'lov usuli
TypeScript'da PaymentChoice komponentini yozing: sarlavha (legend) va uchta radio tugma — "Naqd", "Click", "Payme". Talablar:
- guruhning
namei —useId()dan (sahifada ikkitaPaymentChoicebo'lsa, ular bir-biriga xalaqit bermasin); - har radio o'z yorlig'i bilan (
htmlFor) bog'lansin — id'lar ham bittauseIdasosidan:${id}-naqdkabi; - tanlangan usul state'da (controlled, Formalar I), boshlang'ichi —
"naqd"; AppikkitaPaymentChoicechizsin: "Oldindan to'lov" va "Yetkazishda to'lov".
Yechim
import { useId, useState } from "react";
const METHODS = [
{ value: "naqd", label: "Naqd" },
{ value: "click", label: "Click" },
{ value: "payme", label: "Payme" },
] as const;
type Method = (typeof METHODS)[number]["value"];
export function PaymentChoice({ title }: { title: string }) {
const id = useId();
const [method, setMethod] = useState<Method>("naqd");
return (
<fieldset>
<legend>{title}</legend>
{METHODS.map((m) => (
<span key={m.value}>
<input
type="radio"
id={`${id}-${m.value}`}
name={id}
value={m.value}
checked={method === m.value}
onChange={() => setMethod(m.value)}
/>
<label htmlFor={`${id}-${m.value}`}>{m.label}</label>{" "}
</span>
))}
<p>Tanlandi: {method}</p>
</fieldset>
);
}
export default function App() {
return (
<form>
<PaymentChoice title="Oldindan to'lov" />
<PaymentChoice title="Yetkazishda to'lov" />
</form>
);
}Ekranda:
Oldindan to'lov
Naqd Click Payme
Tanlandi: click
Yetkazishda to'lov
Naqd Click Payme
Tanlandi: naqdHarakat birinchi «Click» yorlig'ini bosdi — faqat birinchi guruh o'zgardi. Ikkala guruhning nomi ham noyob, shuning uchun ikkinchisidagi "Naqd" belgisi joyida qoldi. E'tibor bering: useId .map() ichida emas. U komponent tepasida bir marta chaqirilgan, qolgan id'lar undan yasalgan. METHODS — as const bilan (Union va literal turlar), shuning uchun Method turi "naqd" | "click" | "payme".
11. Real ishda
- Komponent kutubxonalari (Radix, React Aria, MUI kabi) har bir
Input,Checkbox,TabsichidauseIdbilanaria-*bog'lanishlarini o'zi qiladi — siz faqatlabelmatnini berasiz. - Accessibility tekshiruvlari (axe, Lighthouse) takrorlangan id'ni va yorliqsiz maydonni xato deb belgilaydi.
useIdbunday xatolarni sinfi bilan yo'qotadi. - SSR framework'lari (Next.js va boshqalar) da hisoblagich yoki
Math.random()bilan yasalgan id — hydration xatosining eng ko'p uchraydigan sabablaridan biri. - Intervyu savollari: "
useIdnima uchun kerak?", "NegaMath.random()yoki hisoblagich yaramaydi?", "useIdnikeyuchun ishlatsa bo'ladimi?", "identifierPrefixqachon kerak?".
Xulosa
- Qo'lda yozilgan id ikkinchi nusxada to'qnashadi; hisoblagich har renderda va serverda boshqacha bo'ladi.
useId()daraxtdagi o'rinidan barqaror id yasaydi: qayta renderda, StrictMode'da va serverda bir xil.- Bitta id'dan bir nechtasi:
${id}-izoh,${id}-xato—aria-describedbyuchun. - Formatga (
_r_0_) tayanmang; bir sahifada ikki ildiz bo'lsa —identifierPrefix. keyva ma'lumot id'si uchun emas — ular ma'lumotdan keladi.vazifalarda ichkiaria-labelledbyid'lariuseIdda; e2e o'qiydigan id'lar o'zgarmadi.
Keyingi dars: useSyncExternalStore: tashqi manbaga obuna — internet holati va URL kabi React'dan tashqaridagi ma'lumotni to'g'ri o'qishni o'rganamiz.
Manbalar
- React:
useId— react.dev/reference/react/useId - React:
createRoot(identifierPrefix),hydrateRoot— react.dev/reference/react-dom/client - MDN:
aria-describedby,HTMLLabelElement.control— developer.mozilla.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!