TL;DR
Thay useLayoutEffect bằng useEffect — cách này một mình đã giải quyết ~80% trường hợp. Nếu bạn thực sự cần timing layout phía client, hãy dùng một isomorphic hook có fallback về useEffect khi chạy trên server.
// Sửa nhanh — hoạt động cho hầu hết trường hợp
import { useEffect } from 'react';
// Thay thế cái này:
// useLayoutEffect(() => { ... }, [deps]);
// Bằng cái này:
useEffect(() => { ... }, [deps]);
Tại sao cảnh báo này xuất hiện
useLayoutEffect chạy đồng bộ sau khi DOM được vẽ. Vì không có DOM trên server, React không thể thực thi hay serialize callback của nó — nên nó bỏ qua và hiển thị cảnh báo này cho bạn.
Mối nguy thực sự không phải là bản thân cảnh báo. Đó là sự không khớp khi hydration xảy ra sau đó: HTML từ server và HTML được render phía client khác nhau, gây ra hiện tượng nhấp nháy hoặc layout bị vỡ khi tải trang lần đầu.
Bạn thường gặp vấn đề này trong ba tình huống phổ biến:
- Next.js App Router hoặc Pages Router — bất kỳ Client Component nào gọi
useLayoutEffecttrực tiếp - SSR tùy chỉnh với Express/Fastify dùng
renderToStringhoặcrenderToPipeableStream - Thư viện bên thứ ba gọi
useLayoutEffectnội bộ — Radix UI, một số thư viện animation, các phiên bản styled-components cũ
Cách sửa 1 — Dùng useEffect thay thế (giải quyết ~80% trường hợp)
Câu hỏi thực tế: bạn có thực sự cần đọc hoặc ghi DOM trước khi trình duyệt vẽ không? Cập nhật state, đăng ký vào store, gửi analytics — không cái nào trong số đó cần timing layout. useEffect xử lý tất cả mà không phát sinh cảnh báo SSR nào.
// Trước
import { useLayoutEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(window.innerWidth); // không cần timing layout
}, []);
return Width: {width}
;
}
// Sau — cảnh báo biến mất
import { useEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return Width: {width}
;
}
Cách sửa 2 — Isomorphic useLayoutEffect hook
Một số trường hợp thực sự cần timing layout: đo kích thước DOM node, đồng bộ vị trí cuộn trước khi vẽ, định vị tooltip so với anchor. Với những trường hợp đó, hãy dùng một wrapper isomorphic — useLayoutEffect trên client, useEffect trên server.
// hooks/useIsomorphicLayoutEffect.ts
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
// Cách dùng — không cảnh báo, hoạt động đúng ở cả hai môi trường
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
function Tooltip({ anchorRef }: { anchorRef: React.RefObject }) {
useIsomorphicLayoutEffect(() => {
if (!anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect();
// định vị tooltip tương đối với anchor
}, [anchorRef]);
return ...;
}
Pattern này được dùng trong react-redux, react-spring, và hầu hết các thư viện UI tương thích SSR khác. Nó đã được kiểm chứng thực tế — cứ copy về dùng thôi.
Cách sửa 3 — Đánh dấu component chỉ chạy phía client (Next.js App Router)
Một số component không có output hữu ích khi render phía server: floating action button, toast container, canvas animation. Không cần SSR những thứ đó làm gì.
Trong App Router, thêm 'use client' ở đầu file là đủ — useLayoutEffect hoàn toàn ổn bên trong một component chỉ chạy phía client:
// app/components/ToastContainer.tsx
'use client';
import { useLayoutEffect } from 'react';
// An toàn ở đây — file này không bao giờ chạy trên server
Trong Pages Router, dùng dynamic với ssr: false:
// pages/index.tsx
import dynamic from 'next/dynamic';
const HeavyClientComponent = dynamic(
() => import('../components/HeavyClientComponent'),
{ ssr: false }
);
Cách sửa 4 — Tắt cảnh báo từ thư viện bên thứ ba
Đôi khi nguyên nhân là một dependency bạn không thể vá — bạn chỉ kiểm soát nó qua props. Tắt cảnh báo có chọn lọc giúp bạn chờ đến khi có bản fix từ upstream. Hãy làm thật có mục tiêu và ghi chú rõ lý do:
// Chỉ tắt cho vấn đề đã biết của thư viện — ghi chú lý do!
const originalError = console.error;
console.error = (...args: unknown[]) => {
if (
typeof args[0] === 'string' &&
args[0].includes('useLayoutEffect does nothing on the server')
) {
return; // xóa khi thư viện X ra v2.1
}
originalError(...args);
};
Đặt đoạn này trong _app.tsx hoặc file setup test. Hãy gỡ bỏ ngay khi thư viện ra bản vá — cảnh báo bị tắt là món nợ kỹ thuật thầm lặng sẽ làm khó bạn về sau.
Kiểm tra kết quả
- Chạy
next build && next start(hoặc SSR server ở chế độ production). Cảnh báo chỉ xuất hiện ở các đường dẫn SSR, không phải trongnext devkhi render phía client — vì vậy luôn test bản production build. - Quan sát terminal. Không còn dòng
Warning: useLayoutEffect does nothing on the servernữa. - Mở trang với JavaScript bị tắt (DevTools → Settings → Disable JavaScript). HTML từ server phải trông đúng — không có vùng trống, không bị vỡ layout.
- Bật lại JS. Kiểm tra console của trình duyệt xem có cảnh báo hydration mismatch không. Console sạch là bạn đã xong.
Mẹo: hydration mismatch sau khi chuyển sang useEffect
Đã chuyển sang useEffect nhưng vẫn thấy hydration mismatch? Nguyên nhân gần như luôn là một giá trị khác nhau giữa server và client — window.innerWidth, Date.now(), navigator.userAgent.
Cách sửa: khởi tạo state với một giá trị an toàn tương thích với server (null hoặc 0), sau đó cập nhật trong useEffect sau khi hydration hoàn tất.
function ResponsiveComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // server và lần render đầu tiên phía client khớp hoàn toàn
return ...;
}

