Pada aplikasi React modern dengan Server-Side Rendering (SSR) seperti Next.js, kesalahan hydration mismatch sering muncul ketika developer mencoba membaca data dari localStorage untuk menginisialisasi state komponen. Masalah ini ditandai dengan error konsol seperti "Hydration failed because the initial UI does not match what was rendered on the server".
Artikel ini membedah penyebab teknis ketidakcocokan tersebut, mengidentifikasi anti-pattern yang sering terjadi, serta menyajikan dua pendekatan konkret untuk menyelesaikannya secara bersih tanpa menimbulkan Flash of Unstyled Content (FOUC).
Akar Masalah: Server Output vs First Render Pass
Proses SSR dan hidrasi React bekerja dalam urutan berikut:
- Server Render: Server Node.js merender tree komponen menjadi string HTML statis dan mengirimkannya ke browser. Lingkungan server tidak memiliki objek global
windowmaupun APIlocalStorage. - Client Bootstrapping: Browser menerima HTML statis dan menampilkannya seketika (First Contentful Paint).
- Hydration Pass: React runtime diunduh di client dan mengeksekusi render tree pertama kali (initial client render) untuk mencocokkan Virtual DOM dengan DOM HTML hasil kiriman server.
React mewajibkan output dari langkah 1 dan langkah 3 identik secara struktural dan tekstual. Jika Anda membaca localStorage saat inisialisasi state di client, server akan menggunakan nilai fallback (misalnya: light), sementara client langsung menggunakan nilai tersimpan (misalnya: dark). Hasilnya adalah perbedaan atribut atau teks DOM yang memicu hydration error.
Anti-Pattern: Membaca Browser API pada State Initialization
Banyak developer mencoba mengantisipasi ketiadaan window di server dengan menambahkan pengecekan tipe data sederhana:
// ANTI-PATTERN: Menyebabkan hydration mismatch
import { useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('theme') || 'light';
}
return 'light';
});
return <button>Current theme: {theme}</button>;
}Kode di atas tetap gagal. Di server, ekspresi typeof window !== 'undefined' bernilai false dan menghasilkan <button>Current theme: light</button>. Namun saat initial render di browser, kondisi tersebut bernilai true dan mengevaluasi isi localStorage, menghasilkan <button>Current theme: dark</button>. React mendeteksi perbedaan ini saat hidrasi dan membuang node terkait.
Solusi 1: Two-Pass Rendering untuk Komponen Non-Kritis
Untuk komponen yang tidak mempengaruhi layout inti halaman (misalnya: status dismiss banner, keranjang belanja lokal, atau modal preferensi), gunakan pola two-pass rendering. Pola ini menunda render nilai client-side sampai proses hidrasi awal selesai.
Implementasi dengan useSyncExternalStore
React 18 memperkenalkan useSyncExternalStore yang memungkinkan langganan ke external store dengan penanganan hydration yang terjamin tanpa memicu cascading re-render yang tidak perlu.
import { useSyncExternalStore } from 'react';
function subscribe(callback: () => void) {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
}
export function useLocalStorage(key: string, fallback: string) {
return useSyncExternalStore(
subscribe,
() => localStorage.getItem(key) ?? fallback, // Client snapshot
() => fallback // Server snapshot
);
}
// Penggunaan pada komponen
export function Banner() {
const dismissed = useLocalStorage('banner_dismissed', 'false');
if (dismissed === 'true') return null;
return <div className="banner">Informasi Penting</div>;
}Trade-off: Pola ini aman dari error hidrasi, tetapi akan memicu satu render tambahan setelah hidrasi selesai. Hindari pola ini untuk theme atau layout utama karena dapat menimbulkan kedipan visual atau Cumulative Layout Shift (CLS).
Solusi 2: Cookie-Based SSR untuk State Penentu Layout
Untuk state yang menentukan tampilan visual utama seperti dark mode, localStorage adalah medium yang keliru karena nilainya tidak dapat diakses oleh server HTTP. Gunakan HTTP Cookie agar server dapat membaca preferensi pengguna sebelum menghasilkan HTML.
Implementasi pada Next.js (App Router)
Simpan preferensi tema ke dalam cookie menggunakan JavaScript client saat pengguna beralih tema, lalu baca cookie tersebut langsung di Server Component:
// app/layout.tsx (Server Component)
import { cookies } from 'next/headers';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies();
const theme = cookieStore.get('theme')?.value ?? 'light';
return (
<html lang="en" className={theme}>
<body>{children}</body>
</html>
);
}// components/ThemeToggle.tsx (Client Component)
'use client';
export function ThemeToggle({ currentTheme }: { currentTheme: string }) {
function toggleTheme() {
const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
// Set cookie untuk server dan update UI
document.cookie = `theme=${nextTheme}; path=/; max-age=31536000; SameSite=Lax`;
window.location.reload(); // ponytail: trigger refresh sederhana, upgrade ke router.refresh()
}
return <button onClick={toggleTheme}>Ganti Tema</button>;
}Dengan pendekatan ini, server langsung menghasilkan kelas HTML yang sesuai. Tidak ada perbedaan antara output server dan render pass pertama browser, sehingga hydration mismatch dan FOUC terhindar secara menyeluruh.
Lazy Alternative: Inline Blocking Script
Jika aplikasi menggunakan static export atau tidak memungkinkan pemrosesan cookie di server, opsi termudah tanpa dependensi adalah menyisipkan script inline kecil di dalam tag <head> sebelum elemen body dimuat:
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
const t = localStorage.getItem('theme') || 'light';
document.documentElement.classList.add(t);
} catch (e) {}
`,
}}
/>
</head>Script ini dieksekusi secara sinkron sebelum browser memproses rendering CSS dan DOM, mengubah atribut class root sebelum hidrasi React dimulai. Pastikan komponen React Anda tidak merender teks yang bergantung pada nilai state lokal tersebut secara sinkron.
Verifikasi Debugging di Konsol DevTools
Untuk mengonfirmasi perbaikan:
- Buka Chrome DevTools dan periksa tab Console. Error hidrasi akan menampilkan diff visual antara string server HTML dan client vDOM.
- Nonaktifkan JavaScript di browser (Ctrl+Shift+P > Disable JavaScript). Muat ulang halaman untuk memastikan tampilan awal server sudah sesuai dengan state yang diharapkan.
- Periksa tab Network pada request dokumen pertama. Pastikan response HTML mentah sudah mengandung atribut yang sinkron dengan data cookie.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!