ไทย

เชี่ยวชาญ useId hook ของ React คู่มือฉบับสมบูรณ์สำหรับนักพัฒนาในการสร้าง ID ที่เสถียร ไม่ซ้ำใคร และปลอดภัยสำหรับ SSR เพื่อเพิ่มการเข้าถึง (accessibility) และการ hydration

เจาะลึก useId Hook ของ React: วิธีการสร้าง Identifier ที่เสถียรและไม่ซ้ำใคร

ในโลกของการพัฒนาเว็บที่มีการเปลี่ยนแปลงอยู่เสมอ การทำให้เนื้อหาที่เรนเดอร์ฝั่งเซิร์ฟเวอร์กับแอปพลิเคชันฝั่งไคลเอนต์มีความสอดคล้องกันนั้นเป็นสิ่งสำคัญอย่างยิ่ง หนึ่งในความท้าทายที่พบบ่อยและจัดการได้ยากคือการสร้าง identifier ที่ไม่ซ้ำใครและมีความเสถียร ID เหล่านี้มีความสำคัญอย่างยิ่งในการเชื่อมโยง label กับ input, การจัดการ ARIA attributes เพื่อการเข้าถึง (accessibility) และงานอื่นๆ ที่เกี่ยวข้องกับ DOM เป็นเวลาหลายปีที่นักพัฒนาต้องใช้วิธีแก้ปัญหาที่ไม่เหมาะสม ซึ่งมักนำไปสู่ปัญหา hydration mismatch และบั๊กที่น่าหงุดหงิด ขอแนะนำ `useId` hook ของ React 18—ซึ่งเป็นโซลูชันที่เรียบง่ายแต่ทรงพลัง ออกแบบมาเพื่อแก้ปัญหานี้อย่างสง่างามและเด็ดขาด

คู่มือฉบับสมบูรณ์นี้จัดทำขึ้นสำหรับนักพัฒนา React ทั่วโลก ไม่ว่าคุณจะกำลังสร้างแอปพลิเคชันที่เรนเดอร์ฝั่งไคลเอนต์แบบง่ายๆ, ประสบการณ์ Server-Side Rendered (SSR) ที่ซับซ้อนด้วยเฟรมเวิร์กอย่าง Next.js หรือสร้าง component library สำหรับให้คนทั่วโลกใช้งาน การทำความเข้าใจ `useId` ไม่ใช่ทางเลือกอีกต่อไป แต่มันคือเครื่องมือพื้นฐานสำหรับการสร้างแอปพลิเคชัน React ที่ทันสมัย แข็งแกร่ง และเข้าถึงได้

ปัญหาก่อนที่จะมี `useId`: โลกแห่ง Hydration Mismatches

เพื่อให้เข้าใจคุณค่าของ `useId` อย่างแท้จริง เราต้องเข้าใจโลกที่ไม่มีมันเสียก่อน ปัญหาหลักคือความต้องการ ID ที่ไม่ซ้ำกันภายในหน้าเว็บที่เรนเดอร์ แต่ในขณะเดียวกันก็ต้องสอดคล้องกันระหว่างเซิร์ฟเวอร์และไคลเอนต์

ลองพิจารณาคอมโพเนนต์ฟอร์ม input แบบง่ายๆ:


function LabeledInput({ label, ...props }) {
  // เราจะสร้าง ID ที่ไม่ซ้ำกันตรงนี้ได้อย่างไร?
  const inputId = 'some-unique-id';

  return (
    
); }

attribute `htmlFor` บน `

วิธีที่ 1: การใช้ `Math.random()`

ความคิดแรกที่มักจะนึกถึงในการสร้าง ID ที่ไม่ซ้ำกันคือการใช้ค่าสุ่ม


// รูปแบบที่ไม่ควรทำ: อย่าทำแบบนี้!
const inputId = `input-${Math.random()}`;

เหตุผลที่วิธีนี้ล้มเหลว:

วิธีที่ 2: การใช้ตัวนับแบบ Global

วิธีที่ซับซ้อนขึ้นมาเล็กน้อยคือการใช้ตัวนับที่เพิ่มค่าขึ้นเรื่อยๆ


// รูปแบบที่ไม่ควรทำ: มีปัญหาเช่นกัน
let globalCounter = 0;
function generateId() {
  globalCounter++;
  return `component-${globalCounter}`;
}

เหตุผลที่วิธีนี้ล้มเหลว:

ความท้าทายเหล่านี้ชี้ให้เห็นถึงความจำเป็นในการมีโซลูชันที่เป็นของ React เองและทำงานอย่างคาดเดาได้ (deterministic) ซึ่งเข้าใจโครงสร้างของ component tree และนั่นคือสิ่งที่ `useId` มอบให้

ขอแนะนำ `useId`: โซลูชันอย่างเป็นทางการ

`useId` hook จะสร้าง ID ที่เป็นสตริงที่ไม่ซ้ำกันและมีความเสถียรทั้งในการเรนเดอร์ฝั่งเซิร์ฟเวอร์และฝั่งไคลเอนต์ มันถูกออกแบบมาให้เรียกใช้งานที่ระดับบนสุดของคอมโพเนนต์ของคุณเพื่อสร้าง ID สำหรับส่งต่อไปยัง accessibility attributes

Syntax หลักและการใช้งาน

Syntax ของมันเรียบง่ายที่สุดเท่าที่จะเป็นไปได้ โดยไม่รับ arguments ใดๆ และคืนค่าเป็น ID ที่เป็นสตริง


import { useId } from 'react';

function LabeledInput({ label, ...props }) {
  // useId() สร้าง ID ที่ไม่ซ้ำและเสถียร เช่น ":r0:"
  const id = useId();

  return (
    
); } // ตัวอย่างการใช้งาน function App() { return (

Sign Up Form

); }

ในตัวอย่างนี้ `LabeledInput` ตัวแรกอาจได้ ID เป็น `":r0:"` และตัวที่สองอาจได้เป็น `":r1:"` รูปแบบที่แน่นอนของ ID เป็นรายละเอียดการทำงานภายใน (implementation detail) ของ React และไม่ควรยึดติดกับรูปแบบนั้น สิ่งเดียวที่รับประกันได้คือมันจะไม่ซ้ำกันและมีความเสถียร

ประเด็นสำคัญคือ React รับประกันว่าลำดับของ ID ที่สร้างขึ้นบนเซิร์ฟเวอร์และไคลเอนต์จะเหมือนกัน ซึ่งช่วยขจัดปัญหา hydration errors ที่เกี่ยวข้องกับ ID ที่สร้างขึ้นได้อย่างสมบูรณ์

แนวคิดการทำงานเป็นอย่างไร?

ความมหัศจรรย์ของ `useId` อยู่ที่การทำงานที่คาดเดาผลลัพธ์ได้ (deterministic) มันไม่ได้ใช้การสุ่ม แต่จะสร้าง ID ตามเส้นทางของคอมโพเนนต์ภายใน React component tree เนื่องจากโครงสร้างของ component tree จะเหมือนกันทั้งบนเซิร์ฟเวอร์และไคลเอนต์ จึงรับประกันได้ว่า ID ที่สร้างขึ้นจะตรงกัน วิธีการนี้ทนทานต่อลำดับการเรนเดอร์ของคอมโพเนนต์ ซึ่งเป็นจุดอ่อนของวิธีการใช้ตัวนับแบบ global

การสร้าง ID ที่เกี่ยวข้องกันหลายตัวจากการเรียก Hook เพียงครั้งเดียว

ความต้องการทั่วไปคือการสร้าง ID ที่เกี่ยวข้องกันหลายตัวภายในคอมโพเนนต์เดียว ตัวอย่างเช่น input อาจต้องการ ID สำหรับตัวมันเอง และอีก ID หนึ่งสำหรับองค์ประกอบคำอธิบายที่เชื่อมโยงผ่าน `aria-describedby`

คุณอาจอยากที่จะเรียก `useId` หลายครั้ง:


// ไม่ใช่รูปแบบที่แนะนำ
const inputId = useId();
const descriptionId = useId();

แม้ว่าวิธีนี้จะใช้งานได้ แต่รูปแบบที่แนะนำคือการเรียก `useId` เพียงครั้งเดียวต่อคอมโพเนนต์ และใช้ ID พื้นฐานที่ได้มาเป็นคำนำหน้า (prefix) สำหรับ ID อื่นๆ ที่คุณต้องการ


import { useId } from 'react';

function FormFieldWithDescription({ label, description }) {
  const baseId = useId();
  const inputId = `${baseId}-input`;
  const descriptionId = `${baseId}-description`;

  return (
    

{description}

); }

ทำไมรูปแบบนี้ถึงดีกว่า?

ฟีเจอร์เด็ด: Server-Side Rendering (SSR) ที่ไร้ที่ติ

ลองกลับมาดูปัญหาหลักที่ `useId` ถูกสร้างขึ้นมาเพื่อแก้ไข นั่นคือ hydration mismatches ในสภาพแวดล้อม SSR เช่น Next.js, Remix หรือ Gatsby

สถานการณ์: ข้อผิดพลาด Hydration Mismatch

ลองจินตนาการถึงคอมโพเนนต์ที่ใช้วิธี `Math.random()` แบบเก่าในแอปพลิเคชัน Next.js

  1. การเรนเดอร์ฝั่งเซิร์ฟเวอร์: เซิร์ฟเวอร์รันโค้ดของคอมโพเนนต์ `Math.random()` ให้ผลลัพธ์เป็น `0.5` เซิร์ฟเวอร์ส่ง HTML ไปยังเบราว์เซอร์พร้อมกับ ``
  2. การเรนเดอร์ฝั่งไคลเอนต์ (Hydration): เบราว์เซอร์ได้รับ HTML และ JavaScript bundle React เริ่มทำงานบนไคลเอนต์และเรนเดอร์คอมโพเนนต์ใหม่อีกครั้งเพื่อผูก event listeners (กระบวนการนี้เรียกว่า hydration) ในระหว่างการเรนเดอร์นี้ `Math.random()` ให้ผลลัพธ์เป็น `0.9` React สร้าง Virtual DOM ที่มี ``
  3. ความไม่ตรงกัน: React เปรียบเทียบ HTML ที่สร้างจากเซิร์ฟเวอร์ (`id="input-0.5"`) กับ Virtual DOM ที่สร้างจากไคลเอนต์ (`id="input-0.9"`) เมื่อเห็นความแตกต่าง ก็จะแสดงคำเตือน: "Warning: Prop `id` did not match. Server: "input-0.5" Client: "input-0.9""

นี่ไม่ใช่แค่คำเตือนธรรมดาๆ มันสามารถนำไปสู่ UI ที่พัง, การจัดการ event ที่ไม่ถูกต้อง และประสบการณ์ผู้ใช้ที่แย่ React อาจต้องทิ้ง HTML ที่เรนเดอร์จากเซิร์ฟเวอร์และทำการเรนเดอร์ใหม่ทั้งหมดฝั่งไคลเอนต์ ซึ่งเป็นการทำลายประโยชน์ด้านประสิทธิภาพของ SSR

สถานการณ์: การแก้ปัญหาด้วย `useId`

ทีนี้ เรามาดูกันว่า `useId` แก้ปัญหานี้ได้อย่างไร

  1. การเรนเดอร์ฝั่งเซิร์ฟเวอร์: เซิร์ฟเวอร์เรนเดอร์คอมโพเนนต์ มีการเรียกใช้ `useId` โดยอิงจากตำแหน่งของคอมโพเนนต์ใน tree มันจะสร้าง ID ที่เสถียรขึ้นมา สมมติว่าเป็น `":r5:"` เซิร์ฟเวอร์ส่ง HTML พร้อมกับ ``
  2. การเรนเดอร์ฝั่งไคลเอนต์ (Hydration): เบราว์เซอร์ได้รับ HTML และ JavaScript React เริ่มกระบวนการ hydrate โดยจะเรนเดอร์คอมโพเนนต์เดียวกันในตำแหน่งเดียวกันใน tree `useId` hook ก็จะทำงานอีกครั้ง และเนื่องจากผลลัพธ์ของมันคาดเดาได้โดยอิงตามโครงสร้างของ tree มันจึงสร้าง ID เดิมเป๊ะๆ คือ `":r5:"`
  3. ตรงกันอย่างสมบูรณ์: React เปรียบเทียบ HTML ที่สร้างจากเซิร์ฟเวอร์ (`id=":r5:"`) กับ Virtual DOM ที่สร้างจากไคลเอนต์ (`id=":r5:"`) ทั้งสองตรงกันอย่างสมบูรณ์แบบ กระบวนการ Hydration จึงเสร็จสิ้นโดยไม่มีข้อผิดพลาดใดๆ

ความเสถียรนี้เป็นรากฐานสำคัญของคุณค่าของ `useId` มันนำความน่าเชื่อถือและความสามารถในการคาดการณ์มาสู่กระบวนการที่เคยเปราะบาง

พลังพิเศษด้าน Accessibility (a11y) ด้วย `useId`

แม้ว่า `useId` จะมีความสำคัญสำหรับ SSR แต่การใช้งานหลักในชีวิตประจำวันคือการปรับปรุงการเข้าถึง (accessibility) การเชื่อมโยงองค์ประกอบต่างๆ อย่างถูกต้องเป็นพื้นฐานสำหรับผู้ใช้เทคโนโลยีช่วยเหลือ เช่น โปรแกรมอ่านหน้าจอ (screen readers)

`useId` เป็นเครื่องมือที่สมบูรณ์แบบสำหรับการเชื่อมโยง ARIA (Accessible Rich Internet Applications) attributes ต่างๆ

ตัวอย่าง: Modal Dialog ที่เข้าถึงได้

Modal dialog จำเป็นต้องเชื่อมโยงคอนเทนเนอร์หลักกับหัวข้อและคำอธิบายเพื่อให้โปรแกรมอ่านหน้าจอสามารถประกาศได้อย่างถูกต้อง


import { useId, useState } from 'react';

function AccessibleModal({ title, children }) {
  const id = useId();
  const titleId = `${id}-title`;
  const contentId = `${id}-content`;

  return (
    

{title}

{children}
); } function App() { return (

By using this service, you agree to our terms and conditions...

); }

ในที่นี้ `useId` ทำให้มั่นใจได้ว่าไม่ว่า `AccessibleModal` นี้จะถูกนำไปใช้ที่ไหน attributes `aria-labelledby` และ `aria-describedby` จะชี้ไปยัง ID ที่ถูกต้องและไม่ซ้ำกันขององค์ประกอบหัวข้อและเนื้อหาเสมอ ซึ่งมอบประสบการณ์ที่ราบรื่นสำหรับผู้ใช้โปรแกรมอ่านหน้าจอ

ตัวอย่าง: การเชื่อมโยง Radio Buttons ในกลุ่มเดียวกัน

การควบคุมฟอร์มที่ซับซ้อนมักต้องการการจัดการ ID อย่างระมัดระวัง กลุ่มของ radio buttons ควรถูกเชื่อมโยงกับ label กลางเดียวกัน


import { useId } from 'react';

function RadioGroup() {
  const id = useId();
  const headingId = `${id}-heading`;

  return (
    

Select your global shipping preference:

); }

ด้วยการเรียกใช้ `useId` เพียงครั้งเดียวเพื่อใช้เป็นคำนำหน้า เราสามารถสร้างชุดของตัวควบคุมที่สอดคล้องกัน เข้าถึงได้ และไม่ซ้ำใคร ซึ่งทำงานได้อย่างน่าเชื่อถือในทุกที่

ข้อแตกต่างที่สำคัญ: `useId` ไม่ได้มีไว้สำหรับอะไร

พลังที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง การทำความเข้าใจว่าไม่ควรใช้ `useId` ในสถานการณ์ใดก็มีความสำคัญไม่แพ้กัน

อย่าใช้ `useId` สำหรับ List Keys

นี่เป็นข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนาทำ React keys จำเป็นต้องเป็น identifier ที่เสถียรและไม่ซ้ำกันสำหรับข้อมูลชิ้นนั้นๆ ไม่ใช่สำหรับ instance ของคอมโพเนนต์

การใช้งานที่ไม่ถูกต้อง:


function TodoList({ todos }) {
  // รูปแบบที่ไม่ควรทำ: ห้ามใช้ useId สำหรับ keys เด็ดขาด!
  return (
    
    {todos.map(todo => { const key = useId(); // แบบนี้ผิด! return
  • {todo.text}
  • ; })}
); }

โค้ดนี้ละเมิดกฎของ Hooks (คุณไม่สามารถเรียก hook ภายใน loop ได้) แต่ถึงแม้คุณจะปรับโครงสร้างให้แตกต่างออกไป ตรรกะก็ยังคงผิดพลาด `key` ควรผูกอยู่กับไอเท็ม `todo` เอง เช่น `todo.id` ซึ่งจะช่วยให้ React สามารถติดตามไอเท็มได้อย่างถูกต้องเมื่อมีการเพิ่ม, ลบ หรือจัดลำดับใหม่

การใช้ `useId` สำหรับ key จะสร้าง ID ที่ผูกกับตำแหน่งในการเรนเดอร์ (เช่น `

  • ` ตัวแรก) ไม่ใช่ข้อมูล หากคุณจัดลำดับ todos ใหม่ keys จะยังคงอยู่ในลำดับการเรนเดอร์เดิม ทำให้ React สับสนและนำไปสู่บั๊ก

    การใช้งานที่ถูกต้อง:

    
    function TodoList({ todos }) {
      return (
        
      {todos.map(todo => ( // ถูกต้อง: ใช้ ID จากข้อมูลของคุณ
    • {todo.text}
    • ))}
    ); }

    อย่าใช้ `useId` สำหรับการสร้าง ID ของฐานข้อมูลหรือ CSS

    ID ที่สร้างโดย `useId` มีอักขระพิเศษ (เช่น `:`) และเป็นรายละเอียดการทำงานภายในของ React มันไม่ได้มีไว้สำหรับเป็น key ในฐานข้อมูล, เป็น CSS selector สำหรับการจัดสไตล์ หรือใช้กับ `document.querySelector`

    • สำหรับ ID ของฐานข้อมูล: ใช้ไลบรารีอย่าง `uuid` หรือกลไกการสร้าง ID ดั้งเดิมของฐานข้อมูลของคุณ สิ่งเหล่านี้คือ universally unique identifiers (UUIDs) ที่เหมาะสำหรับการจัดเก็บข้อมูลแบบถาวร
    • สำหรับ CSS Selectors: ใช้ CSS classes การพึ่งพา ID ที่สร้างขึ้นโดยอัตโนมัติเพื่อการจัดสไตล์เป็นแนวทางที่เปราะบาง

    `useId` vs. ไลบรารี `uuid`: ควรใช้อะไรเมื่อไหร่

    คำถามที่พบบ่อยคือ "ทำไมไม่ใช้ไลบรารีอย่าง `uuid` ไปเลยล่ะ?" คำตอบอยู่ที่วัตถุประสงค์ที่แตกต่างกันของมัน

    คุณสมบัติ React `useId` ไลบรารี `uuid`
    กรณีการใช้งานหลัก สร้าง ID ที่เสถียรสำหรับองค์ประกอบ DOM โดยหลักแล้วสำหรับ accessibility attributes (`htmlFor`, `aria-*`) สร้าง universally unique identifiers สำหรับข้อมูล (เช่น key ของฐานข้อมูล, object identifiers)
    ความปลอดภัยสำหรับ SSR ใช่ ทำงานแบบคาดเดาได้และรับประกันว่าจะเหมือนกันทั้งบนเซิร์ฟเวอร์และไคลเอนต์ ไม่ ทำงานโดยอาศัยการสุ่มและจะทำให้เกิด hydration mismatches หากเรียกใช้ระหว่างการเรนเดอร์
    ความไม่ซ้ำกัน ไม่ซ้ำกันภายในหนึ่งการเรนเดอร์ของแอปพลิเคชัน React ไม่ซ้ำกันในระดับสากลทั่วทุกระบบและทุกช่วงเวลา (มีความน่าจะเป็นที่จะชนกันต่ำมาก)
    ควรใช้เมื่อไหร่ เมื่อคุณต้องการ ID สำหรับองค์ประกอบในคอมโพเนนต์ที่คุณกำลังเรนเดอร์ เมื่อคุณสร้างรายการข้อมูลใหม่ (เช่น todo ใหม่, ผู้ใช้ใหม่) ที่ต้องการ identifier ที่ถาวรและไม่ซ้ำกัน

    กฎง่ายๆ: ถ้า ID มีไว้สำหรับบางสิ่งที่อยู่ภายในผลลัพธ์การเรนเดอร์ของคอมโพเนนต์ React ของคุณ ให้ใช้ `useId` ถ้า ID มีไว้สำหรับชิ้นส่วนของข้อมูลที่คอมโพเนนต์ของคุณกำลังเรนเดอร์อยู่ ให้ใช้ UUID ที่เหมาะสมซึ่งสร้างขึ้นเมื่อข้อมูลนั้นถูกสร้างขึ้น

    สรุปและแนวทางปฏิบัติที่ดีที่สุด

    `useId` hook เป็นเครื่องยืนยันถึงความมุ่งมั่นของทีม React ในการปรับปรุงประสบการณ์ของนักพัฒนาและช่วยให้สามารถสร้างแอปพลิเคชันที่แข็งแกร่งยิ่งขึ้น มันช่วยแก้ปัญหาที่ยุ่งยากในอดีต—นั่นคือการสร้าง ID ที่เสถียรในสภาพแวดล้อมแบบเซิร์ฟเวอร์/ไคลเอนต์—และมอบโซลูชันที่เรียบง่าย ทรงพลัง และติดตั้งมาพร้อมกับเฟรมเวิร์ก

    การทำความเข้าใจวัตถุประสงค์และรูปแบบการใช้งานของมันจะช่วยให้คุณเขียนคอมโพเนนต์ที่สะอาดขึ้น เข้าถึงได้มากขึ้น และน่าเชื่อถือมากขึ้น โดยเฉพาะอย่างยิ่งเมื่อทำงานกับ SSR, component libraries และฟอร์มที่ซับซ้อน

    ประเด็นสำคัญและแนวทางปฏิบัติที่ดีที่สุด:

    • ควรใช้ `useId` เพื่อสร้าง ID ที่ไม่ซ้ำกันสำหรับ accessibility attributes เช่น `htmlFor`, `id` และ `aria-*`
    • ควรเรียกใช้ `useId` เพียงครั้งเดียวต่อคอมโพเนนต์และใช้ผลลัพธ์เป็นคำนำหน้าหากคุณต้องการ ID ที่เกี่ยวข้องกันหลายตัว
    • ควรนำ `useId` มาใช้ในแอปพลิเคชันใดๆ ที่ใช้ Server-Side Rendering (SSR) หรือ Static Site Generation (SSG) เพื่อป้องกัน hydration errors
    • ไม่ควรใช้ `useId` เพื่อสร้าง `key` props เมื่อเรนเดอร์ลิสต์ Keys ควรมาจากข้อมูลของคุณ
    • ไม่ควรยึดติดกับรูปแบบเฉพาะของสตริงที่ `useId` คืนค่ากลับมา เพราะเป็นรายละเอียดการทำงานภายใน
    • ไม่ควรใช้ `useId` สำหรับการสร้าง ID ที่ต้องจัดเก็บในฐานข้อมูลหรือใช้สำหรับการจัดสไตล์ CSS ให้ใช้ classes สำหรับการจัดสไตล์และไลบรารีอย่าง `uuid` สำหรับ identifier ของข้อมูล

    ครั้งต่อไปที่คุณกำลังจะใช้ `Math.random()` หรือตัวนับที่สร้างขึ้นเองเพื่อสร้าง ID ในคอมโพเนนต์ ให้หยุดและจำไว้ว่า: React มีวิธีที่ดีกว่า ใช้ `useId` และสร้างสรรค์ผลงานด้วยความมั่นใจ