IlmHamroh
JavaScript Full-stack/17-qism. React asoslari37/45-dars19 daqiqa
Mundarija (30)

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. Bitta useId dan bir nechta id yasash mumkin: ${id}-izoh, ${id}-xato. Ro'yxatdagi key uchun esa useId ishlatilmaydi — key ma'lumotdan keladi.

Bu darsda

  • Qo'lda yozilgan va hisoblagich bilan yasalgan id nega React'da buzilishini ko'rsata olasiz.
  • useId bilan label, input, aria-describedby ni bog'laysiz.
  • Bitta id'dan bir nechta bog'langan id yasaysiz.
  • identifierPrefix va id formati haqida bilasiz, unga tayanmaslikni tushunasiz.
  • vazifalar dagi aria-labelledby id'larini useId ga 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:

jsx
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:

text
id'lar: 2
zaxira yorlig'i -> asosiy

Ekranda:

text
Telefon
Zaxira telefon

label.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:

js
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:

text
<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:

jsx
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:

text
render 0 maydon-0
render 1 maydon-1

Ekranda:

text
Ism Qayta chizish

Har 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:

jsx
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:

text
Ism maydon-0
Ism maydon-1
Telefon maydon-2
Telefon maydon-3
(2) ['maydon-1', 'maydon-3']

Ekranda:

text
Ism
Telefon

Ikki 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? useRef qiymati 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

jsx
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:

text
render 0 _r_0_
render 1 _r_0_

Ekranda:

text
Ism Qayta chizish

useId() 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

jsx
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:

text
zaxira yorlig'i -> zaxira

Ekranda:

text
Telefon
Zaxira telefon

Endi 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 --> S

Diagramma 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:

jsx
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:

text
Telefon
Masalan: +998 90 000 00 00
Raqam +998 bilan boshlansin
Tekshirish

Malika 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 uchun className.
  • document.querySelector bilan id bo'yicha qidirmang — React'da elementni ref bilan 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:

jsx
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:

text
Mehmonlar soni _bron-r_0_

Ekranda:

text
Mehmonlar soni

identifierPrefix — 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:

jsx
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:

text
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:

text
Ism

Tarjima: "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:

jsx
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:

text
(2) ['_R_1_', '_R_2_']

Ekranda:

text
Ism
Telefon

Ogohlantirish 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:

jsx
// ❌ 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:

text
  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:

diff
+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):

tsx
  // 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:

text
- 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):

text
✅ 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:

text
refactor: aria-labelledby id'lari useId bilan

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

jsx
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
jsx
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:

text
Mehmonlar soni -> 1 dan 12 gacha

Ekranda:

text
Mehmonlar soni 1 dan 12 gacha

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

jsx
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
jsx
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:

text
hamma yorliq bog'langan: true

Ekranda:

text
Ism
Telefon
Osh
Manti
  1. htmlFor="mehmon" — yorliq hech qaysi maydonga bog'lanmagan edi (bunday id yo'q). Endi htmlFor={id}.
  2. Hisoblagich har renderda yangi id berardi — useId().
  3. 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 name i — useId() dan (sahifada ikkita PaymentChoice bo'lsa, ular bir-biriga xalaqit bermasin);
  • har radio o'z yorlig'i bilan (htmlFor) bog'lansin — id'lar ham bitta useId asosidan: ${id}-naqd kabi;
  • tanlangan usul state'da (controlled, Formalar I), boshlang'ichi — "naqd";
  • App ikkita PaymentChoice chizsin: "Oldindan to'lov" va "Yetkazishda to'lov".
Yechim
tsx
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:

text
Oldindan to'lov
Naqd Click Payme
Tanlandi: click
Yetkazishda to'lov
Naqd Click Payme
Tanlandi: naqd

Harakat 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, Tabs ichida useId bilan aria-* bog'lanishlarini o'zi qiladi — siz faqat label matnini berasiz.
  • Accessibility tekshiruvlari (axe, Lighthouse) takrorlangan id'ni va yorliqsiz maydonni xato deb belgilaydi. useId bunday 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: "useId nima uchun kerak?", "Nega Math.random() yoki hisoblagich yaramaydi?", "useId ni key uchun ishlatsa bo'ladimi?", "identifierPrefix qachon 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-describedby uchun.
  • Formatga (_r_0_) tayanmang; bir sahifada ikki ildiz bo'lsa — identifierPrefix.
  • key va ma'lumot id'si uchun emas — ular ma'lumotdan keladi.
  • vazifalar da ichki aria-labelledby id'lari useId da; 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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React useId: label va aria atributlari uchun barqaror, noyob id — IlmHamroh