Hydration error pada Expo Router terjadi ketika representasi HTML yang dihasilkan di lingkungan Node.js (server-side rendering) tidak identik dengan struktur Virtual DOM saat React pertama kali melakukan mounting di browser. Ketidakcocokan ini memicu peringatan konsol seperti Hydration failed because the initial UI does not match what was rendered on the server atau Text content does not match server-rendered HTML. Pada mode produksi, kondisi ini memaksa React membuang DOM server dan melakukan render ulang dari awal (client bailout), yang merusak performa First Contentful Paint (FCP) dan berpotensi memicu layout shift.
Akar Masalah: Desinkronisasi Virtual DOM pada Expo Router SSR
Server-side rendering pada Expo Router mengeksekusi kode JavaScript universal menggunakan runtime Node.js sebelum menyajikan dokumen HTML ke browser. Masalah muncul ketika komponen mengeksekusi logika render yang bergantung pada API browser atau status dinamis yang tidak tersedia di server.
1. Inspeksi Runtime Prematur (window dan localStorage)
Mengakses global object browser langsung di siklus render menyebabkan percabangan UI yang berbeda:
// Problem: Server menghasilkan fallback, client menghasilkan UI dinamis
function UserGreeting() {
if (typeof window === 'undefined') {
return <Text>Tamu</Text>;
}
return <Text>{localStorage.getItem('username') || 'Tamu'}</Text>;
}Server menghasilkan <span>Tamu</span>, sedangkan browser langsung merender nama pengguna dari cache lokal saat hidrasi berjalan, menghasilkan mismatch pada node teks.
2. Dynamic Dimensions dan Media Queries
Pemanggilan Dimensions.get('window') pada Node.js mengembalikan nilai default (biasanya width: 0, height: 0 atau window standar) karena Node.js tidak memiliki viewport fisik. Ketika browser membuka halaman, ukuran layar sebenarnya (misalnya 1920x1080) dihitung seketika, menyebabkan pohon komponen yang berbasis kondisi lebar layar berbeda total antara server dan client.
3. Format Tanggal dan Locale
Penggunaan new Date().toLocaleString() mengandalkan zona waktu dan konfigurasi locale mesin. Jika server Expo dieksekusi di instance container dengan zona waktu UTC, sedangkan browser client berada di zona GMT+7 (WIB), output string tanggal dipastikan berbeda.
Solusi 1: Pola Two-Pass Rendering Menggunakan useIsMounted
Gunakan pendekatan two-pass rendering untuk komponen yang mutlak memerlukan API sisi klien. Pola ini memastikan server dan render awal klien menghasilkan markup identik, lalu memperbarui UI setelah siklus mounting selesai.
// hooks/useIsMounted.ts
import { useState, useEffect } from 'react';
export function useIsMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}Implementasi pada komponen target:
// components/ClientTimestamp.tsx
import { Text } from 'react-native';
import { useIsMounted } from '@/hooks/useIsMounted';
export function ClientTimestamp({ timestamp }: { timestamp: number }) {
const isMounted = useIsMounted();
// ponytail: render placeholder statis saat SSR dan pass hidrasi pertama
if (!isMounted) {
return <Text>Memuat tanggal...</Text>;
}
return <Text>{new Date(timestamp).toLocaleString('id-ID')}</Text>;
}Trade-off: Pendekatan ini menyebabkan satu siklus render tambahan di klien. Batasi penggunaannya hanya pada leaf component (komponen ujung pohon UI) untuk meminimalkan re-render.
Solusi 2: State External Sinkron via useSyncExternalStore
Untuk data eksternal seperti viewport browser atau status media query, gunakan API resmi React useSyncExternalStore. API ini menyediakan parameter khusus getServerSnapshot yang menjamin nilai deterministic saat dieksekusi oleh Expo Router SSR.
// hooks/useMediaQuerySSR.ts
import { useSyncExternalStore } from 'react';
function subscribe(callback: () => void) {
const media = window.matchMedia('(min-width: 768px)');
media.addEventListener('change', callback);
return () => media.removeEventListener('change', callback);
}
export function useIsDesktop() {
return useSyncExternalStore(
subscribe,
() => window.matchMedia('(min-width: 768px)').matches,
() => false // getServerSnapshot: nilai fallback deterministik untuk server
);
}Metode ini mencegah tearing UI dan menghilangkan warning hidrasi karena React mengendalikan siklus pembacaan snapshot eksternal antara server snapshot dan client snapshot secara aman.
Solusi 3: Isolasi Komponen via Ekstensi Platform (.web.tsx)
Jika komponen bergantung berat pada API browser yang tidak dapat disimulasikan di server atau native, pisahkan implementasi menggunakan resolusi file Metro bundler.
Sidebar.native.tsx: Digunakan untuk iOS dan Android.Sidebar.web.tsx: Digunakan khusus untuk platform web.Sidebar.tsx: Digunakan sebagai fallback default jika platform tidak spesifik.
// components/Layout/Sidebar.web.tsx
import { View, Text } from 'react-native';
export default function Sidebar() {
return (
<aside style={{ width: 280, display: 'flex', flexDirection: 'column' }}>
<Text>Navigasi Desktop Web</Text>
</aside>
);
}Bundler Expo akan memetakan file secara eksklusif sesuai target build, memangkas eksekusi kode native yang berpotensi menyebabkan error dependensi di lingkungan Node.js SSR.
Solusi 4: Isolasi CSS Layout untuk Mencegah Cumulative Layout Shift (CLS)
Menyembunyikan atau menampilkan elemen UI menggunakan kondisi JavaScript berbasis lebar layar via inline style menyebabkan konten melompat ketika hydration selesai. Selesaikan masalah layout shift menggunakan media query CSS statis murni melalui React Native for Web StyleSheet.
// components/ResponsiveContainer.tsx
import { StyleSheet, View } from 'react-native';
export function ResponsiveContainer({ children }: { children: React.ReactNode }) {
return <View style={styles.container}>{children}</View>;
}
const styles = StyleSheet.create({
container: {
width: '100%',
maxWidth: 1200,
marginHorizontal: 'auto',
// Menggunakan CSS unit bawaan web melalui React Native Web
paddingHorizontal: 16,
},
});Jika membutuhkan layout responsif yang drastis, hindari percabangan kondisi JS di root render. Gunakan CSS class murni atau modul style yang dievaluasi langsung oleh CSS engine browser tanpa menunggu JavaScript React aktif.
Validasi dan Pengujian Build SSR
Hidrasi tidak dapat divalidasi hanya melalui mode development client biasa. Jalankan proses export static web untuk menguji konsistensi rendering:
# 1. Buat static export web dengan mode SSR/SSG
npx expo export -p web
# 2. Uji hasil build lokal menggunakan server statis lokal
npx serve dist --singleBuka browser Console pada Network tab, pilih opsi Fast 3G untuk memperlambat eksekusi JavaScript. Jika halaman selesai di-parse namun tampilan berkedip atau memunculkan stack trace Hydration failed di konsol browser, lacak komponen dengan menonaktifkan hydration sementara menggunakan atribut suppressHydrationWarning hanya pada elemen teks spesifik yang tidak dapat dihindari (misalnya rendering hash dinamis acak).
// Solusi darurat hanya untuk elemen leaf string spesifik
<Text suppressHydrationWarning>{dynamicId}</Text>Gunakan suppressHydrationWarning secara hemat. Atribut ini hanya membisukan error pada atribut dan teks level 1, bukan memperbaiki error struktural Virtual DOM.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!