UUID acak bikin hydration mismatch di SSR ketika nilai yang dirender di server berbeda dengan nilai yang dirender ulang di client saat proses hydration. Kasus paling umum adalah memanggil generator ID acak seperti crypto.randomUUID(), Math.random(), atau helper sejenis langsung di dalam fungsi render komponen.

Akibatnya bukan hanya warning di console. ID yang berubah antara server dan client dapat merusak keterkaitan label dengan input, atribut ARIA seperti aria-describedby, stabilitas key pada list, sampai memicu UI yang terlihat “benar” tetapi perilakunya tidak konsisten. Di React dan Next.js, ini sering muncul sebagai hydration warning yang sulit direproduksi karena kadang hanya terjadi pada kondisi tertentu.

Mengapa UUID acak saat render memicu hydration mismatch

Pada SSR, alurnya secara umum seperti ini:

  1. Server merender komponen menjadi HTML.
  2. HTML dikirim ke browser.
  3. Client menjalankan JavaScript, lalu React melakukan hydration dengan asumsi struktur dan output render awal sama dengan HTML dari server.

Masalah muncul jika komponen menghasilkan nilai non-deterministik saat render. Server menghasilkan satu UUID, client menghasilkan UUID lain. React lalu melihat bahwa atribut atau struktur yang diharapkan tidak cocok dengan DOM yang sudah ada.

Contoh sederhana:

function Field() {
  const id = crypto.randomUUID();

  return (
    <div>
      <label htmlFor={id}>Email</label>
      <input id={id} name="email" />
    </div>
  );
}

Jika server menghasilkan "a1" dan client menghasilkan "b2", HTML awal berisi for="a1" dan id="a1", tetapi render client mengharapkan b2. Inilah sumber mismatch.

Prinsip umumnya: pada SSR, output render awal harus deterministik. Nilai yang dipakai untuk membentuk HTML awal harus sama di server dan client.

Gejala yang sering terlihat di UI

Tidak semua mismatch langsung tampak sebagai kerusakan besar. Justru yang berbahaya adalah gejala halus yang sulit dilacak.

1. Warning hydration di console

Ini gejala paling jelas. React biasanya memberi peringatan bahwa properti, teks, atau struktur DOM hasil hydration tidak cocok dengan hasil render server.

2. Label tidak lagi terhubung ke input yang benar

Jika htmlFor dan id berubah, klik pada label bisa gagal memfokuskan input yang dimaksud, atau hubungan aksesibilitas menjadi tidak konsisten.

3. Atribut ARIA mengarah ke elemen yang salah

Komponen seperti input dengan helper text, error message, dialog, tabs, atau accordion sering memakai aria-describedby, aria-labelledby, dan aria-controls. Jika ID berbeda, pembaca layar bisa kehilangan konteks, walau tampilan visual tampak normal.

4. List rendering terasa “aneh”

Jika UUID acak dipakai sebagai key di dalam render, React menganggap setiap item adalah elemen baru pada setiap render. Efeknya bisa berupa state lokal item hilang, input di dalam list kehilangan fokus, animasi reset, atau performa memburuk karena remount terus-menerus.

5. Bug hanya muncul di environment tertentu

Mismatch sering lebih sulit direproduksi pada halaman dengan data async, conditional rendering, streaming, atau komponen yang hanya tampil setelah interaksi tertentu. Karena itu akar masalahnya sering luput: sumbernya bukan UI, melainkan ID yang tidak stabil.

Titik rawan: key, id, aria linkage, form field, dan list rendering

UUID acak sebagai atribut id

Ini adalah sumber mismatch paling langsung. Atribut id dipakai untuk menghubungkan elemen yang harus saling mereferensikan. Karena referensi ini berbasis string, perbedaan sekecil apa pun membuat relasi gagal.

function PasswordField() {
  const inputId = crypto.randomUUID();
  const helpId = crypto.randomUUID();

  return (
    <div>
      <label htmlFor={inputId}>Password</label>
      <input id={inputId} aria-describedby={helpId} type="password" />
      <p id={helpId}>Minimal 12 karakter.</p>
    </div>
  );
}

Kode di atas tampak masuk akal, tetapi tidak aman untuk SSR bila UUID dibuat saat render.

UUID acak sebagai key

key harus stabil antar-render untuk item yang sama. Jika Anda menulis:

{items.map(item => (
  <Row key={crypto.randomUUID()} item={item} />
))}

maka setiap render menghasilkan key baru. Bahkan tanpa SSR, ini sudah salah karena React kehilangan kemampuan melacak identitas item. Dengan SSR, masalah makin parah karena server dan client hampir pasti memakai key yang berbeda.

Pilihan yang benar adalah memakai ID dari data:

{items.map(item => (
  <Row key={item.id} item={item} />
))}

Form field dinamis

Form builder, field array, atau komponen reusable sering perlu ID unik untuk setiap field. Kesalahan umum adalah menganggap “unik” selalu berarti “acak”. Untuk SSR, yang dibutuhkan sering kali bukan acak, melainkan stabil.

Contoh salah vs benar di React/Next.js

Contoh salah: generate UUID di render

export default function NewsletterForm() {
  const emailId = crypto.randomUUID();
  const hintId = crypto.randomUUID();

  return (
    <form>
      <label htmlFor={emailId}>Email</label>
      <input id={emailId} aria-describedby={hintId} name="email" />
      <small id={hintId}>Kami tidak akan membagikan email Anda.</small>
    </form>
  );
}

Masalahnya sederhana: output render tidak deterministik. Pada SSR, komponen ini rentan mismatch.

Contoh benar: pakai ID stabil dari server atau data

Jika data form atau field berasal dari server, kirimkan ID yang sama ke client dan gunakan ID itu saat render.

export default function NewsletterForm({ fieldIds }) {
  const { emailId, hintId } = fieldIds;

  return (
    <form>
      <label htmlFor={emailId}>Email</label>
      <input id={emailId} aria-describedby={hintId} name="email" />
      <small id={hintId}>Kami tidak akan membagikan email Anda.</small>
    </form>
  );
}

Pola ini aman karena server dan client memakai nilai yang sama untuk HTML awal.

Contoh benar: gunakan ID dari domain data

function AddressList({ addresses }) {
  return (
    <ul>
      {addresses.map((address) => (
        <li key={address.id}>
          <strong>{address.label}</strong>: {address.value}
        </li>
      ))}
    </ul>
  );
}

Jika item memang mewakili entitas bisnis atau record database, gunakan identifier yang sudah melekat pada entitas tersebut.

Contoh benar: pakai useId untuk linkage DOM, bukan untuk identity data

import { useId } from 'react';

function EmailField() {
  const inputId = useId();
  const hintId = `${inputId}-hint`;

  return (
    <div>
      <label htmlFor={inputId}>Email</label>
      <input id={inputId} aria-describedby={hintId} name="email" />
      <p id={hintId}>Gunakan email aktif.</p>
    </div>
  );
}

useId dirancang untuk menghasilkan ID yang konsisten antara server dan client selama struktur pohon render tetap konsisten. Ini cocok untuk menghubungkan elemen DOM dalam komponen, misalnya label ke input, atau input ke helper/error text.

Namun, useId bukan pengganti ID data. Jangan gunakan untuk:

  • key item list
  • primary key data
  • identifier yang harus bertahan di luar lifecycle komponen
  • ID yang perlu dipakai ulang lintas request atau disimpan ke database

Kapan useId aman, dan kapan tidak cukup

Aman digunakan untuk

  • Keterkaitan label dan input
  • aria-describedby, aria-labelledby, aria-controls
  • ID internal komponen yang hanya dibutuhkan saat render UI

Tidak cukup atau tidak tepat untuk

  • Key list: key harus berasal dari identitas item, bukan dari urutan render komponen.
  • ID domain: misalnya ID order, user, comment, atau row database.
  • Kasus dengan struktur render yang berubah drastis: jika urutan atau keberadaan komponen berubah antara server dan client karena kondisi yang tidak konsisten, ID berbasis urutan render juga bisa ikut bergeser.

Intinya, useId aman bila masalah Anda adalah DOM linkage, bukan data identity.

Strategi pencegahan yang paling praktis

1. Jangan generate nilai acak di render SSR

Hindari memanggil crypto.randomUUID(), Math.random(), timestamp baru, atau fungsi non-deterministik lain langsung di body komponen yang dirender di server.

Ini termasuk helper yang tampak tidak berbahaya, misalnya:

function makeId(prefix) {
  return `${prefix}-${Math.random().toString(36).slice(2)}`;
}

Jika dipanggil saat render SSR, hasilnya tetap tidak stabil.

2. Gunakan ID dari data atau server

Jika item sudah punya ID dari backend, gunakan itu. Jika belum, buat ID di layer yang lebih stabil:

  • saat data dibuat di backend
  • saat memproses data sebelum render
  • saat menyusun props di server

Tujuannya agar server dan client merujuk pada nilai yang sama, bukan sama-sama “menghasilkan” nilai baru secara independen.

3. Untuk linkage UI lokal, pilih useId

Jika kebutuhan Anda hanya menghubungkan elemen di satu komponen, useId biasanya lebih tepat daripada UUID acak.

4. Jika ID hanya dibutuhkan setelah mount, buat di effect atau event handler

Ada kasus di mana nilai acak memang tidak perlu ikut dalam HTML awal. Misalnya ID untuk draft lokal, correlation ID untuk aksi client-only, atau identifier untuk data yang baru dibuat setelah klik pengguna. Dalam kasus seperti ini, buat nilai tersebut setelah hydration selesai, bukan saat SSR render.

import { useEffect, useState } from 'react';

function ClientOnlyDraftId() {
  const [draftId, setDraftId] = useState(null);

  useEffect(() => {
    setDraftId(crypto.randomUUID());
  }, []);

  return <p>Draft: {draftId ?? 'belum dibuat'}</p>;
}

Pendekatan ini menghindari mismatch karena nilai acak tidak ikut menentukan HTML server. Trade-off-nya, nilai tersebut tidak tersedia pada render awal.

5. Waspadai conditional rendering yang berbeda antara server dan client

Bahkan jika Anda memakai useId, mismatch tetap bisa terjadi bila struktur komponen berbeda saat dirender di server dan client. Penyebab umum:

  • percabangan berdasarkan window, document, atau API browser lain
  • membaca ukuran viewport saat render
  • menggunakan locale, timezone, atau feature flag yang berbeda
  • mengandalkan data yang hanya tersedia di client pada render pertama

Jika urutan komponen berubah, urutan pembuatan ID juga bisa berubah.

Prinsip yang sama berlaku di framework SSR lain

Meski contoh di artikel ini fokus pada React dan Next.js, masalahnya bukan spesifik ke satu framework. Semua SSR framework bergantung pada prinsip yang sama: HTML awal harus cocok dengan hasil aktivasi di browser.

Di framework lain pun, sumber masalahnya serupa:

  • nilai acak dibuat saat render template atau komponen server-side
  • client menghitung ulang nilai yang berbeda saat hydration
  • atribut, teks, atau struktur DOM tidak lagi sinkron

Jadi aturannya tetap relevan secara umum: jangan pakai ID acak pada output render awal kecuali framework menyediakan mekanisme deterministic ID yang memang dirancang untuk SSR.

Kesalahan umum yang sering luput

“Saya hanya butuh ID unik, jadi UUID pasti aman”

Unik tidak sama dengan stabil. Untuk SSR, stabilitas lintas server-client lebih penting daripada keacakan.

“Warning hydration bisa diabaikan, UI tetap jalan”

Tidak selalu. Kadang React dapat memulihkan mismatch tertentu, tetapi hasil akhirnya bisa berupa DOM yang ditambal, event binding yang membingungkan, atau aksesibilitas yang rusak tanpa terlihat jelas secara visual.

“Saya pakai UUID sebagai key agar tidak bentrok”

Ini justru menghilangkan fungsi utama key. Key bukan sekadar harus unik, tetapi harus merepresentasikan identitas item yang sama dari render ke render.

“Saya simpan UUID di state awal”

Inisialisasi state yang bergantung pada nilai acak saat render awal tetap dapat bermasalah pada SSR jika server dan client menginisialisasi nilai yang berbeda untuk markup awal. Jika nilainya tidak perlu ikut dalam HTML awal, buat setelah mount.

Checklist debugging untuk hydration mismatch yang sulit direproduksi

Jika warning hydration muncul sesekali dan sulit dilacak, gunakan checklist berikut:

  1. Cari semua sumber non-deterministik di render
    Telusuri pemakaian crypto.randomUUID(), Math.random(), Date.now(), formatter berbasis timezone saat render, dan helper util yang menghasilkan string unik.
  2. Audit atribut yang bergantung pada ID
    Periksa id, htmlFor, aria-describedby, aria-labelledby, aria-controls, dan target anchor atau fragment.
  3. Periksa semua key pada list
    Pastikan key berasal dari data yang stabil, bukan dari index untuk list yang bisa berubah, dan bukan dari generator acak.
  4. Bandingkan HTML server dengan DOM setelah hydration
    Lihat elemen yang diperingatkan React. Fokus pada atribut yang berbeda, bukan hanya pada komponen tempat warning muncul.
  5. Uji komponen dalam mode SSR yang realistis
    Bug sering tidak muncul pada rendering client-only. Reproduksi pada environment yang benar-benar melakukan SSR.
  6. Waspadai percabangan server vs client
    Cari kondisi seperti typeof window !== 'undefined' di render path yang memengaruhi struktur subtree.
  7. Periksa library UI pihak ketiga
    Beberapa komponen membuat ID internal. Pastikan library tersebut SSR-safe atau sediakan ID stabil melalui props jika didukung.
  8. Tambahkan logging sementara pada nilai ID
    Log ID yang dihasilkan pada server dan pada client untuk komponen yang dicurigai. Jika nilainya berbeda pada render awal, Anda sudah menemukan akar masalahnya.

Rekomendasi praktis yang aman dipakai sehari-hari

  • Gunakan ID dari data/server untuk entitas, record, dan item list.
  • Gunakan useId untuk linkage DOM internal komponen pada React.
  • Jangan gunakan UUID acak sebagai key.
  • Jangan generate ID acak langsung di render SSR.
  • Jika nilai acak hanya dibutuhkan di client, buat nilainya setelah mount atau saat event user terjadi.

Penutup

UUID acak bikin hydration mismatch di SSR bukan karena UUID itu buruk, tetapi karena ia sering dipakai pada tempat yang membutuhkan determinisme. Pada render awal SSR, yang Anda perlukan bukan nilai yang sangat unik, melainkan nilai yang sama persis di server dan client.

Untuk React dan Next.js, aturan amannya sederhana: pakai ID stabil dari data atau server untuk identitas, pakai useId untuk linkage DOM internal, dan jangan generate nilai acak di jalur render yang ikut membentuk HTML awal. Dengan pemisahan ini, Anda bisa menghindari warning hydration yang membingungkan sekaligus menjaga aksesibilitas dan stabilitas UI.