Bẫy: server render không có localStorage nên dựng HTML bằng giá trị mặc định; client dựng lại bằng giá trị đã lưu. Hai bản khác nhau → React báo hydration mismatch và có thể vứt HTML server để render lại toàn bộ trên client.
Sai lầm kinh điển là đọc storage ngay khi khởi tạo state:
// sai khi SSR: server và client cho kết quả khác nhau
const [theme, setTheme] = useState(localStorage.getItem('theme') ?? 'light')Hướng xử lý:
- Render lần đầu bằng giá trị mặc định, đọc storage trong useEffect rồi mới setState — lần render đầu khớp server, sau đó mới cập nhật.
- Với Zustand persist, dùng cờ hasHydrated (qua onRehydrateStorage hoặc persist.hasHydrated()) để chỉ hiển thị UI phụ thuộc dữ liệu đã lưu sau khi rehydrate xong; trước đó render skeleton.
- Riêng theme thường phải chống nhấp nháy bằng script inline chạy trước khi paint, gắn data-theme lên <html> — vì effect chạy sau paint sẽ thấy chớp màu.
Hai lưu ý khác: localStorage đồng bộ nên đừng ghi mỗi keystroke (throttle lại), và đừng persist token nhạy cảm hay dữ liệu server đã có cache riêng.