Hydration error terjadi saat pohon DOM yang dihasilkan oleh server (SSR) tidak identik dengan representasi Virtual DOM pada render awal di client. Pada aplikasi video player minimalis—seperti antarmuka fokus bebas rekomendasi bergaya NoSuggest—masalah ini paling sering dipicu oleh pembacaan langsung API browser seperti localStorage atau window untuk mengambil preferensi volume, resume timestamp, atau status toggle panel.
Akar Masalah: Mengapa LocalStorage Merusak Hydration
Server node runtime tidak memiliki akses ke objek window atau localStorage. Jika komponen membaca storage langsung saat inisialisasi state, server akan menggunakan nilai fallback default (misalnya tombol toggle feed bernilai false atau panel tersembunyi).
Ketika file HTML tiba di browser, React menjalankan proses hydration untuk mengaitkan event listener ke elemen DOM yang ada. Jika client langsung membaca storage lokal yang bernilai true pada siklus render pertama, React mendeteksi perbedaan atribut atau struktur node antara HTML kiriman server dan output render client. Hasilnya adalah React hydration mismatch warning, potensi layout thrashing, atau UI glitch di mana elemen interaktif gagal merespons aksi klik.
Contoh Kode Masalah (Anti-Pattern)
Berikut adalah implementasi umum yang memicu hydration error secara konsisten:
// VideoPlayer.jsx - IMPLEMENTASI SALAH
import { useState } from 'react';
export function VideoPlayer({ src }) {
// Membaca localStorage langsung saat inisialisasi
const [autoplay, setAutoplay] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('player_autoplay') === 'true';
}
return false;
});
return (
<div className="player-container">
<video src={src} autoPlay={autoplay} controls />
<button onClick={() => setAutoplay(!autoplay)}>
{/* Server merender: Autoplay: OFF, Client merender: Autoplay: ON */}
Autoplay: {autoplay ? 'ON' : 'OFF'}
</button>
</div>
);
}Solusi 1: Two-Pass Rendering dengan useEffect
Pendekatan standar adalah memastikan render pertama client persis sama dengan render server, lalu memperbarui state di client pass kedua via useEffect. Hook ini hanya dieksekusi setelah hydration selesai.
// VideoPlayerTwoPass.jsx
import { useState, useEffect } from 'react';
export function VideoPlayerTwoPass({ src }) {
const [autoplay, setAutoplay] = useState(false);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
const saved = localStorage.getItem('player_autoplay');
if (saved !== null) {
setAutoplay(saved === 'true');
}
}, []);
if (!isMounted) {
// Render placeholder atau default state yang identik dengan server
return (
<div className="player-container">
<video src={src} autoPlay={false} controls />
<button disabled>Loading settings...</button>
</div>
);
}
return (
<div className="player-container">
<video src={src} autoPlay={autoplay} controls />
<button onClick={() => {
const nextState = !autoplay;
setAutoplay(nextState);
localStorage.setItem('player_autoplay', String(nextState));
}}>
Autoplay: {autoplay ? 'ON' : 'OFF'}
</button>
</div>
);
}Solusi 2: Hook useSyncExternalStore (Rekomendasi Modern)
React 18 memperkenalkan hook bawaan useSyncExternalStore. Solusi ini menghindari flashing re-render ganda dan menyediakan pemisahan eksplisit antara client snapshot dan server snapshot.
// useLocalStorage.js
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
}
export function useLocalStorage(key, initialValue) {
const getSnapshot = () => {
const val = localStorage.getItem(key);
return val !== null ? val : initialValue;
};
const getServerSnapshot = () => initialValue;
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}Implementasi pada komponen UI video player:
// MinimalVideoPlayer.jsx
import { useLocalStorage } from './useLocalStorage';
export function MinimalVideoPlayer({ src }) {
const hideRecommendations = useLocalStorage('hide_recommendations', 'true') === 'true';
return (
<div className="focus-mode-wrapper">
<video src={src} controls className="w-full" />
{/* DOM server dan render hydration pertama dipastikan sinkron */}
{hideRecommendations ? null : (
<aside className="recommendations-drawer">
<p>Daftar Video Terkait</p>
</aside>
)}
</div>
);
}Checklist Audit SSR Guard
Gunakan checklist teknis berikut untuk mengaudit komponen antarmuka yang mengakses API client:
- Tunda Evaluasi State Browser: Jangan panggil
localStorage,sessionStorage,window.matchMedia, ataunavigatorpada blok badan komponen atau di dalam argumen defaultuseState. - Validasi Fallback Snapshot: Pastikan fungsi
getServerSnapshotpadauseSyncExternalStoreselalu mengembalikan tipe data primitif deterministik (bukan nilai acak atau turunan tanggal runtime). - Identifikasi Atribut Dinamis: Pastikan atribut HTML seperti
style,className, ataudisabledtidak bergantung pada nilai storage lokal sebelum event mount selesai. - Gunakan SupressHydrationWarning Secara Selektif: Gunakan atribut
suppressHydrationWarninghanya pada elemen teks independen (seperti localized timestamp) jika perbedaan render dapat ditoleransi dan bukan pada node struktural.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!