Hydration error pada dark mode biasanya terjadi ketika server merender HTML dengan tema awal tertentu, tetapi saat JavaScript berjalan di browser, state tema yang sebenarnya berbeda. Akibatnya, markup awal dari server tidak cocok dengan hasil render pertama di klien.
Pola ini sering muncul pada theme toggle yang membaca localStorage atau prefers-color-scheme langsung saat komponen dirender. Di framework SSR seperti React dengan Next.js, mismatch ini bukan sekadar peringatan di console: UI bisa berkedip, ikon toggle berubah sendiri, class dark terlambat diterapkan, atau teks awal berbeda dari hasil akhir.
Memahami penyebab hydration error
Pada aplikasi SSR, ada dua tahap render:
- Server render: server menghasilkan HTML awal yang dikirim ke browser.
- Client hydration: React di browser mengambil alih HTML tersebut dan menghubungkannya dengan state, event handler, dan hasil render pertama di klien.
Hydration berjalan mulus jika hasil render pertama di klien sama dengan HTML dari server. Masalah muncul saat tema awal dihitung dengan sumber data yang hanya tersedia di browser, misalnya:
localStorage.getItem('theme')window.matchMedia('(prefers-color-scheme: dark)')- akses langsung ke
windowataudocument
Server tidak punya akses ke API browser tersebut. Karena itu, server biasanya merender fallback, misalnya tema light. Saat browser meng-hydrate, komponen membaca preferensi pengguna dan langsung merender dark. Inilah sumber mismatch.
Gejala yang umum terlihat
- Peringatan seperti Text content did not match atau Hydration failed because the initial UI does not match what was rendered on the server.
- Ikon tema berubah sesaat setelah halaman tampil.
- Class
darkpadahtmlataubodybaru ditambahkan setelah mount. - Terjadi flash: halaman tampil terang dulu lalu berubah gelap.
- Komponen yang bergantung pada tema, seperti logo, syntax highlighting, atau gambar, ikut berganti setelah load.
Contoh implementasi yang memicu mismatch
Contoh berikut terlihat masuk akal, tetapi berisiko memicu hydration error:
import { useState } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '🌙' : '☀️'}
</button>
);
}Masalahnya ada pada inisialisasi state yang membaca API browser saat render. Di lingkungan SSR:
- server tidak bisa membaca
localStoragedanmatchMedia, - hasil render server dan klien bisa berbeda,
- teks tombol, ikon, atau atribut DOM menjadi tidak konsisten saat hydration.
Bahkan jika Anda menambahkan pengecekan typeof window !== 'undefined', mismatch masih bisa terjadi. Pengecekan itu mencegah error runtime di server, tetapi tidak menjamin HTML awal sama antara server dan klien.
Strategi 1: Render aman setelah mount
Strategi paling sederhana adalah menunda bagian UI yang bergantung pada preferensi browser sampai komponen sudah ter-mount di klien. Tujuannya: hasil render server dan render awal klien dibuat sama terlebih dahulu.
import { useEffect, useState } from 'react';
export default function ThemeToggle() {
const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState('light');
useEffect(() => {
setMounted(true);
const saved = localStorage.getItem('theme');
const resolved = saved
? saved
: window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
setTheme(resolved);
}, []);
useEffect(() => {
if (!mounted) return;
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}, [mounted, theme]);
if (!mounted) {
return <button aria-label="Toggle theme" disabled>…</button>;
}
return (
<button
aria-label="Toggle theme"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme === 'dark' ? '🌙' : '☀️'}
</button>
);
}Mengapa cara ini bekerja?
- Server selalu merender output yang stabil, misalnya tombol placeholder.
- Render pertama di klien juga sama dengan output server.
- Setelah mount, barulah preferensi browser dibaca dan UI diperbarui.
Kelebihan
- Paling mudah diterapkan.
- Tidak butuh perubahan di sisi server.
- Cocok jika toggle tema bukan elemen yang kritis pada above the fold.
Kekurangan
- Masih bisa ada flash of incorrect theme jika class tema diterapkan terlambat.
- Toggle mungkin kosong, nonaktif, atau berubah sesaat setelah halaman tampil.
- UX kurang mulus dibanding pendekatan yang menyelaraskan tema sejak HTML awal.
Strategi 2: Inisialisasi tema di server
Jika Anda ingin SSR konsisten, server perlu mengetahui tema awal sebelum merender HTML. Ini biasanya berarti preferensi tema harus tersedia dalam request, misalnya melalui cookie.
Alurnya seperti ini:
- Pengguna memilih tema.
- Klien menyimpan pilihan ke cookie.
- Pada request berikutnya, server membaca cookie tersebut.
- Server merender HTML dengan class atau atribut tema yang benar sejak awal.
Contoh pendekatan lintas stack:
// pseudo-code server
function resolveThemeFromRequest(req) {
const cookieTheme = req.cookies.theme;
if (cookieTheme === 'dark' || cookieTheme === 'light') {
return cookieTheme;
}
return 'light';
}
function renderPage(req) {
const theme = resolveThemeFromRequest(req);
return `
<html class="${theme === 'dark' ? 'dark' : ''}">
<body>
<div id="app">...</div>
</body>
</html>
`;
}Di sisi klien, saat user menekan toggle, Anda sinkronkan state UI, class DOM, dan cookie:
function applyTheme(theme) {
document.documentElement.classList.toggle('dark', theme === 'dark');
document.cookie = `theme=${theme}; path=/; max-age=31536000; samesite=lax`;
localStorage.setItem('theme', theme);
}Dengan cara ini, request berikutnya sudah membawa preferensi tema ke server, sehingga HTML SSR lebih konsisten.
Kapan pendekatan ini cocok
- Aplikasi SSR yang ingin menghindari mismatch sejak awal.
- Desain yang sangat sensitif terhadap flash tema.
- Halaman dengan banyak elemen yang berubah berdasarkan tema.
Keterbatasan
- Pada kunjungan pertama, server mungkin tetap belum tahu preferensi pengguna jika cookie belum ada.
- Jika Anda juga menggunakan
prefers-color-scheme, perlu aturan prioritas yang jelas antara cookie, localStorage, dan preferensi sistem. - Kompleksitas meningkat karena state tema tersebar di server dan klien.
Strategi 3: Gunakan cookie sebagai sumber kebenaran utama
Untuk aplikasi SSR, cookie biasanya lebih cocok daripada localStorage sebagai sumber kebenaran awal. Alasannya sederhana: cookie ikut terkirim dalam request, sedangkan localStorage hanya tersedia di browser.
Pola yang cukup aman:
- Server membaca cookie
themeuntuk menentukan HTML awal. - Klien membaca nilai yang sama dari DOM atau props hasil SSR.
- Saat user mengganti tema, klien memperbarui DOM dan cookie.
- LocalStorage, jika dipakai, hanya sebagai cache tambahan, bukan penentu SSR.
Jika Anda memakai React/Next.js atau stack serupa, prinsip ini tetap sama meski API pengambilan cookie berbeda.
Prioritas preferensi yang disarankan
- Pertama: pilihan eksplisit pengguna yang tersimpan di cookie.
- Kedua: jika belum ada cookie, gunakan preferensi sistem/browser.
- Ketiga: fallback default aplikasi, misalnya light.
Dengan urutan ini, perilaku aplikasi lebih mudah diprediksi dan lebih mudah di-debug.
Strategi 4: Fallback tanpa flash sebelum hydration
Masalah berikutnya adalah flash. Walaupun hydration error sudah dihindari, halaman bisa tetap tampil dengan tema yang salah sepersekian detik sebelum React aktif.
Solusinya adalah menerapkan tema sedini mungkin, idealnya sebelum konten dirender penuh. Secara umum, ini bisa dilakukan dengan skrip kecil di dokumen HTML yang berjalan lebih awal dan mengatur class tema pada document.documentElement.
<script>
(function () {
try {
var saved = localStorage.getItem('theme');
var theme = saved;
if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (e) {
// abaikan jika storage tidak tersedia
}
})();
</script>Pendekatan ini berguna untuk mengurangi flash, tetapi ada trade-off penting:
- Bagus untuk UX karena tema diterapkan lebih cepat.
- Namun, jika SSR masih merender markup berdasarkan asumsi berbeda, Anda tetap bisa mengalami mismatch pada komponen React tertentu.
Jadi, skrip awal ini bukan pengganti sinkronisasi SSR. Ia lebih tepat dipakai sebagai lapisan tambahan untuk mengurangi kedipan visual.
Prinsip praktis: hindari hydration mismatch dulu, lalu kurangi flash. Jangan membalik urutannya.
Contoh pola implementasi yang lebih aman
Berikut contoh ringkas yang memisahkan tema ter-resolve dari UI toggle. Komponen menerima initialTheme dari server bila tersedia, lalu menyelaraskan ke klien setelah mount.
import { useEffect, useState } from 'react';
export default function ThemeController({ initialTheme = 'light' }) {
const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState(initialTheme);
useEffect(() => {
setMounted(true);
const saved = localStorage.getItem('theme');
const clientTheme = saved || initialTheme;
setTheme(clientTheme);
}, [initialTheme]);
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
if (mounted) {
localStorage.setItem('theme', theme);
document.cookie = `theme=${theme}; path=/; max-age=31536000; samesite=lax`;
}
}, [theme, mounted]);
return (
<button
type="button"
aria-label="Toggle theme"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{mounted ? (theme === 'dark' ? '🌙' : '☀️') : '…'}
</button>
);
}Pola ini tidak sempurna untuk semua kasus, tetapi cukup aman karena:
- server punya nilai awal yang eksplisit,
- komponen tidak memaksa membaca API browser saat render SSR,
- persistensi ke cookie membantu request selanjutnya tetap konsisten.
Trade-off UX vs konsistensi SSR
Tidak ada satu strategi yang selalu terbaik. Pilihan bergantung pada prioritas aplikasi Anda.
Jika prioritasnya konsistensi SSR
- Gunakan cookie agar server tahu tema awal.
- Render UI berdasarkan tema yang sama di server dan klien.
- Hindari membaca localStorage untuk menentukan output awal komponen.
Ini paling aman untuk mencegah hydration error, tetapi implementasinya lebih kompleks.
Jika prioritasnya implementasi sederhana
- Tunda render komponen bertema sampai mount.
- Terima bahwa toggle atau sebagian UI mungkin muncul belakangan.
Ini mudah dan cukup efektif, tetapi UX awal bisa sedikit kurang halus.
Jika prioritasnya mengurangi flash visual
- Tambahkan skrip awal untuk menerapkan class tema sebelum hydration.
- Tetap pastikan markup React tidak bergantung pada asumsi yang berbeda.
Ini membantu tampilan awal, tetapi tidak cukup jika sumber mismatch ada di struktur atau teks komponen.
Checklist debugging hydration error untuk theme toggle
- Apakah ada akses ke
window,document,localStorage, ataumatchMediasaat render awal? - Apakah server merender tema light, tetapi klien langsung merender dark?
- Apakah teks, ikon, atribut
class, ataudata-themeberbeda antara SSR dan render pertama klien? - Apakah toggle menampilkan label atau ikon berbeda sebelum dan sesudah mount?
- Apakah Anda hanya memperbaiki error runtime dengan
typeof window, tetapi belum menyamakan output SSR dan klien? - Apakah cookie tema sudah benar-benar terbaca di request server?
- Apakah ada beberapa sumber kebenaran sekaligus, misalnya cookie, localStorage, context, dan class DOM yang bisa saling bertabrakan?
- Apakah stylesheet atau utilitas CSS bergantung pada class
darkyang diterapkan terlalu lambat?
Kesalahan yang sering terjadi
Menganggap localStorage cocok untuk SSR
localStorage cocok untuk menyimpan preferensi di browser, tetapi bukan sumber data yang bisa dipakai server untuk render awal. Jika dipakai sebagai penentu tema pertama, mismatch mudah terjadi.
Mengubah class tema tanpa menyamakan state UI
Ada kasus ketika class dark di html sudah benar, tetapi komponen toggle masih menampilkan ikon yang salah karena state React belum sinkron. Pastikan DOM dan state UI bergerak bersama.
Terlalu cepat merender konten yang bergantung pada tema
Logo, ilustrasi, atau teks yang berbeda per tema sebaiknya tidak dirender berdasarkan API browser sebelum mount, kecuali server memang sudah tahu temanya.
Kesimpulan
Mencegah hydration error dari theme toggle dan preferensi browser pada dasarnya adalah soal menyamakan sumber kebenaran antara server dan klien. Jika server merender satu tema, tetapi klien langsung memilih tema lain dari localStorage atau prefers-color-scheme, hydration mismatch sangat mungkin terjadi.
Pendekatan yang paling aman adalah:
- gunakan cookie jika server perlu tahu tema awal,
- tunda render bagian yang sensitif sampai mount jika data hanya ada di browser,
- tambahkan skrip awal hanya untuk mengurangi flash, bukan untuk menutupi mismatch struktural.
Jika Anda ragu memilih strategi, gunakan aturan praktis ini: untuk SSR yang konsisten, server harus tahu tema awal; untuk implementasi cepat, render aman setelah mount; untuk UX yang halus, tambahkan fallback awal tanpa flash.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!