Menu

Hook useId trong React: ID duy nhất cho label và ARIA

useId trả về một id duy nhất và ổn định cho mỗi instance của component, để label, input và gợi ý aria-describedby trỏ được tới nhau kể cả khi component xuất hiện nhiều lần. Tìm hiểu vì sao Math.random và biến đếm hỏng khi render phía server, và vì sao useId không dùng làm key cho danh sách.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

useId trả về một id duy nhất cho mỗi instance của component, và giữ nguyên id đó qua mọi lần render. Dùng nó để nối một <label> với <input> của nó, hoặc một input với dòng gợi ý qua aria-describedby, khi component có thể xuất hiện nhiều lần trên một trang.

Cùng một component render hai lần và nhận hai id khác nhau, in ra dưới mỗi field. Click vào chữ "Confirm password" và con trỏ nhảy sang input thứ hai, vì htmlFor của nó chỉ khớp với id của input đó. Viết cứng id="password" thay cho useId() thì cả hai label đều trỏ tới input đầu tiên.

Cú pháp

const id = useId();

useId không nhận đối số và trả về một chuỗi. Gọi nó ở cấp cao nhất của component, giống mọi hook khác. Định dạng chính xác của nó là chi tiết nội bộ và đã thay đổi giữa các phiên bản: React 18 tạo ra :r1:, còn React 19.2 tạo ra _r_1_ cho component render lần đầu trong trình duyệt và một id bắt đầu bằng _R_, dựng từ vị trí trong cây, cho component render ở máy chủ. Đừng bao giờ phân tích hay dựa vào hình dạng của nó.

Vì sao không dùng Math.random hay biến đếm

Id cho khả năng truy cập phải khớp giữa HTML mà máy chủ gửi về và cây mà React dựng trong trình duyệt. Hai cách tạo id dễ nghĩ ra nhất đều không qua được yêu cầu đó.

// Changes on every render, and differs between server and browser
const id = 'field-' + Math.random().toString(36).slice(2);

// The server's counter keeps growing across requests, the browser starts at 0
let nextId = 0;
const id = 'field-' + nextId++;

Khi render phía server (Next.js, chế độ framework của React Router, bất kỳ thiết lập hydrateRoot nào), máy chủ in id="field-4817" vào HTML, lần render đầu trong trình duyệt tính ra field-0, và React báo lỗi hydration mismatch. useId dựng id từ vị trí của component trong cây, vốn giống hệt nhau ở cả hai phía.

Ngay cả khi không có máy chủ, một id tạo ra trong lúc render cũng có thể đổi ở mỗi lần render. Ví dụ này cho thấy sự khác biệt mà không cần tới máy chủ:

Click nút vài lần. Giá trị useId đứng yên, còn id từ biến đếm tăng dần theo mỗi lần render, nên thứ gì trỏ tới id cũ (một aria-describedby, một label) giờ chẳng trỏ tới đâu cả. Bọc biến đếm trong useState(() => nextId++) sẽ sửa được chuyện render lại, nhưng không sửa được việc lệch với máy chủ.

Nhiều id từ một lần gọi

Một component có nhiều field không cần gọi useId nhiều lần. Tạo một phần gốc rồi thêm hậu tố cho từng phần tử.

Gõ một từ không có @ vào field email: lỗi xuất hiện và aria-describedby của input trỏ tới nó, nên trình đọc màn hình sẽ đọc lỗi khi input được focus. Render <SignupForm /> hai lần trong App thì mỗi bản sao nhận id gốc riêng.

Không dùng làm key cho danh sách

Key và id giải quyết hai vấn đề khác nhau. Key cho React biết phần tử nào là phần tử nào qua các lần render, nên nó phải đến từ dữ liệu. useId cho một id mỗi instance component, và bạn không thể gọi nó bên trong map.

// Wrong: breaks the rules of hooks, and the key is unrelated to the item
{todos.map((todo) => <Todo key={useId()} todo={todo} />)}

// Right: the key comes from the data
{todos.map((todo) => <Todo key={todo.id} todo={todo} />)}

Khi dữ liệu không có id, hãy tạo id lúc tạo phần tử (crypto.randomUUID() trong event handler thêm phần tử đó), không phải trong lúc render. Trang danh sách và key giải thích vì sao key phải gắn liền với phần tử của nó.

Nhiều root React trên một trang

Nếu hai ứng dụng React riêng biệt render trên cùng một trang, id của chúng có thể trùng nhau. Hãy cho mỗi root một tiền tố:

createRoot(document.getElementById('cart'), { identifierPrefix: 'cart-' });
createRoot(document.getElementById('chat'), { identifierPrefix: 'chat-' });

Khi render phía server, truyền cùng một identifierPrefix cho renderer phía server và cho hydrateRoot để cả hai phía tạo ra cùng các id.

Lỗi thường gặp

Render cây khác nhau ở máy chủ và trong trình duyệt. useId phụ thuộc vào vị trí của component, nên một nhánh như typeof window === 'undefined' ? <A /> : <B /> nằm phía trên một field có thể làm lệch id giữa hai lần render. Giữ cây giống nhau trong lúc hydrate và đổi sau đó, bên trong một effect.

Tìm phần tử bằng id của nó. document.getElementById(id) vẫn chạy, nhưng ref mới là cách của React để chạm tới một node DOM và hoàn toàn không cần id.

Dùng nó như một giá trị ngẫu nhiên. Id này duy nhất trong ứng dụng chứ không ngẫu nhiên, và có thể đoán được từ cây. Đừng dùng nó cho token bảo mật, khóa cache hay bất cứ thứ gì lưu qua nhiều phiên.

Khi nào nên dùng

Dùng useId mỗi khi một component dùng lại được cần thuộc tính id: các field của form dựng từ một component dùng nhiều lần, tooltip nối bằng aria-describedby, dialog có aria-labelledby, tab có aria-controls. Khi id chỉ để nối label với input, bạn cũng có thể bỏ id và đặt input bên trong label (<label>Name <input /></label>); dùng useId khi các phần tử không lồng vào nhau được.

Câu hỏi thường gặp

useId trong React dùng để làm gì?

Nó trả về một chuỗi duy nhất cho instance component đó và giữ nguyên qua mọi lần render. Bạn dùng nó để nối các phần tử với nhau qua id: htmlFor trên label, aria-describedby trên input, aria-labelledby trên dialog.

Vì sao không dùng Math.random() hay biến đếm để tạo id?

Chúng cho ra giá trị khác nhau ở máy chủ và trong trình duyệt, nên trang render phía server và bản đã hydrate không khớp nhau, và React báo lỗi hydration mismatch. Math.random() còn đổi ở mỗi lần render. useId lấy id từ vị trí của component trong cây, vốn giống nhau ở cả hai nơi.

Có thể dùng useId làm key trong danh sách không?

Không. Key phải đến từ dữ liệu để React khớp đúng một phần tử qua các lần render. useId được gọi một lần cho mỗi component, và gọi nó bên trong map đằng nào cũng vi phạm quy tắc của hook. Hãy dùng id riêng của phần tử.

Làm sao lấy nhiều id từ một lần gọi useId?

Gọi useId một lần rồi thêm hậu tố: ${id}-name, ${id}-email. Phần gốc là duy nhất, nên các chuỗi có hậu tố cũng vậy.

Có thể dùng id từ useId trong CSS selector không?

Nên tránh. Id này dùng để nối các phần tử trong DOM, và định dạng chính xác của nó là chi tiết nội bộ đã thay đổi giữa các phiên bản React. Hãy định kiểu bằng class, và tìm phần tử bằng ref thay vì querySelector.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu