Akar Masalah: Falsy Coercion Angka 0 pada JSX SSR
Penggunaan teknik bitwise mask sering diaplikasikan untuk mengelola permission atau state kompleks secara ringkas. Namun, saat ekspresi bitwise diintegrasikan langsung ke dalam conditional rendering JSX, masalah hidrasi pada framework Server-Side Rendering (SSR) seperti Next.js atau Remix tidak dapat dihindari.
JavaScript mengevaluasi operasi bitwise seperti flags & PERMISSION_MASK menjadi representasi integer 32-bit. Jika kondisi tidak terpenuhi, hasil evaluasi adalah integer 0, bukan boolean false.
Di lingkungan React, tipe data null, undefined, dan boolean (true/false) diabaikan dan tidak dimasukkan ke dalam DOM tree. Sebaliknya, angka—termasuk angka 0—dianggap sebagai representasi data valid dan dirender langsung sebagai text node. Pada SSR, server menyisipkan string 0 ke dalam markup HTML awal:
// Hasil evaluasi JavaScript:
flags & MASK // menghasilkan integer 0
// JSX evaluasi:
{flags & MASK && <EditPanel />}
// dievaluasi menjadi: {0 && <EditPanel />} -> 0
// Output server HTML:
<div>0</div>Ketika client melakukan hidrasi, variasi pembacaan state lokal, re-render dinamis, atau komponen kondisional di client sering kali memicu disparitas struktur DOM antara server HTML dan Virtual DOM client. Hasilnya adalah rusaknya proses hydration reconciliation.
Reproduksi: Kode Bermasalah dan Pesan Kesalahan Hydration
Berikut adalah implementasi umum micro-optimization ala C yang memicu hydration mismatch di React SSR:
// permissions.ts
export const CAN_READ = 1 << 0; // 1
export const CAN_WRITE = 1 << 1; // 2
export const CAN_DELETE = 1 << 2; // 4
// UserBadge.tsx
interface Props {
userFlags: number;
}
export function UserBadge({ userFlags }: Props) {
return (
<div className="badge-container">
<span>Aktivitas Akun</span>
{/* Masalah: evaluasi menghasilkan 0 jika userFlags tidak memiliki bit CAN_DELETE */}
{userFlags & CAN_DELETE && (
<button className="btn-danger">Hapus Entri</button>
)}
</div>
);
}Ketika kode di atas dirender pada server dengan userFlags = CAN_READ (nilai 1), server mengembalikan HTML berikut:
<div class="badge-container"><span>Aktivitas Akun</span>0</div>Jika di sisi client komponen dijalankan dalam kondisi state berbeda atau tree reconciler mencoba mencocokkan virtual node terhadap text node "0", React menampilkan error hidrasi pada console browser:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Expected server HTML to contain a matching text node for "" in <div>.
See more info here: https://react.dev/link/hydration-mismatchSelain merusak tampilan antarmuka dengan memunculkan karakter "0" yang tidak diinginkan, hydration mismatch memaksa React membuang cabang DOM server dan melakukan synchronous client re-render, yang memperburuk metrik Largest Contentful Paint (LCP) dan Cumulative Layout Shift (CLS).
Solusi: Evaluasi Boolean Eksplisit
Hilangkan asumsi bahwa JavaScript short-circuit logic && selalu menghasilkan boolean. Terapkan evaluasi eksplisit sebelum mengoper nilai ke conditional rendering JSX.
1. Pengecekan Komparasi Eksplisit (Rekomendasi Utama)
Pendekatan paling aman dan mudah dibaca adalah membandingkan hasil operasi bitwise langsung dengan nilai integer 0 atau mask target:
export function UserBadge({ userFlags }: Props) {
const canDelete = (userFlags & CAN_DELETE) !== 0;
return (
<div className="badge-container">
<span>Aktivitas Akun</span>
{canDelete && <button className="btn-danger">Hapus Entri</button>}
</div>
);
}2. Penggunaan Operator Ternary
Ternary operator memaksa cabang rendering mengembalikan null secara eksplisit saat kondisi tidak terpenuhi, sehingga server tidak mencetak text node angka 0:
export function UserBadge({ userFlags }: Props) {
return (
<div className="badge-container">
<span>Aktivitas Akun</span>
{(userFlags & CAN_DELETE) ? (
<button className="btn-danger">Hapus Entri</button>
) : null}
</div>
);
}3. Boolean Coercion Eksplisit Menggunakan Double NOT
Jika operator logical AND (&&) tetap dipertahankan, konversikan hasil bitwise menjadi tipe data boolean menggunakan Boolean() atau operator double negation (!!):
export function UserBadge({ userFlags }: Props) {
return (
<div className="badge-container">
<span>Aktivitas Akun</span>
{Boolean(userFlags & CAN_DELETE) && (
<button className="btn-danger">Hapus Entri</button>
)}
</div>
);
}Pencegahan Otomatis di CI/CD: ESLint dan TypeScript Rules
Pencegahan manual rentan terlewat dalam code review. Konfigurasikan static analysis tool pada pipeline CI/CD untuk mendeteksi leaked rendering dan evaluasi non-boolean secara otomatis.
1. Konfigurasi ESLint: react/jsx-no-leaked-render
Rule ini mencegah ekspresi non-boolean digunakan di sisi kiri operator && pada JSX.
// .eslintrc.json
{
"rules": {
"react/jsx-no-leaked-render": [
"error",
{ "validStrategies": ["ternary", "coerce"] }
]
}
}Dengan rule ini, penulisan {flags & MASK && <Component />} akan memicu compile error karena nilai ekspresi bitwise bertipe number, bukan boolean.
2. Konfigurasi TypeScript ESLint: strict-boolean-expressions
Aktifkan rule dari @typescript-eslint untuk memastikan seluruh conditional check menggunakan strict boolean:
// .eslintrc.json
{
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/strict-boolean-expressions": [
"error",
{
"allowNumber": false,
"allowString": false,
"allowNullableObject": false
}
]
}
}Integrasi kedua rule ini memastikan pattern micro-optimization yang berisiko merusak SSR tertolak otomatis pada level linting sebelum masuk ke branch produksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!