Một vấn đề kinh điển khi làm tính năng Login: Khi user F5 lại trang, Header hiện chữ "Đăng nhập" trong tích tắc (100-300ms) rồi mới nhảy sang avatar User. ➡️ Trải nghiệm tệ, thiếu chuyên nghiệp.
Bài viết này sẽ hướng dẫn cách xử lý triệt để vấn đề này trong Next.js (App Router & Pages Router) và React SAP.
1. Nguyên nhân gây "Nháy" (FOUC)
FOUC (Flash of Unauthenticated Content) xảy ra do độ trễ khi Client check auth:
- Server trả về HTML tĩnh: Lúc này chưa biết ai đang login (trừ khi dùng SSR Auth), nên mặc định render giao diện "Guest".
- JS chạy ở Client: Gọi API
/mehoặc đọc LocalStorage. - State Update: Sau khi có data user, React re-render lại thành giao diện "User".
Khoảng thời gian (2) chính là lúc user thấy cái nút "Đăng nhập" thừa thãi đó.
2. Giải pháp 1: Khởi tạo State từ Cookie (Client-Side)
Nếu không dùng SSR, bạn nên lưu một flag nhỏ ở Cookie (ví dụ isLoggedIn=true) để Client đọc ngay lập tức khi khởi tạo state, trước khi cả useEffect chạy.
// hooks/useAuth.ts
import Cookies from 'js-cookie';
const useAuth = () => {
// 1. "Đoán" trạng thái ban đầu dựa trên Cookie (Sync reads)
// Cookie có thể đọc ngay lập tức, nhanh hơn LocalStorage/API
const [user, setUser] = useState(() => {
const hasToken = Cookies.get('accessToken');
return hasToken ? { temp: true } : null; // Giả lập user user tạm
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// 2. Fetch data thật để lấy full profile
api.getProfile()
.then(u => setUser(u))
.catch(() => setUser(null)) // Nếu token hết hạn thì logout
.finally(() => setIsLoading(false));
}, []);
return { user, isLoading };
};Tuy nhiên, cách này vẫn có thể sai nếu Token hết hạn mà Cookie vẫn còn. Nhưng nó giải quyết được 90% cảm giác khó chịu.
3. Giải pháp 2: Loading Skeleton (Layout Shift is Better than Wrong Content)
Thà hiện một cái ô vuông màu xám (Skeleton) còn hơn hiện sai nút.
// Components/Header.tsx
export default function Header() {
const { user, isLoading } = useAuth();
return (
<header>
<Logo />
<div className="auth-section">
{isLoading ? (
// ✅ Hiện Skeleton khi đang xác thực (Kích thước bằng đúng nút thật)
<div className="w-10 h-10 rounded-full bg-gray-200 animate-pulse" />
) : user ? (
<UserAvatar user={user} />
) : (
<LoginButton />
)}
</div>
</header>
);
}4. Giải pháp 3: Next.js Middleware (The Ultimate Fix) 🛡️
Với Next.js App Router, cách tốt nhất là chặn ngay từ Server. Middleware chạy trước khi trang được render.
Luồng đi:
- User request
/dashboard. - Middleware check Cookie
accessToken.- Có -> Cho qua, có thể inject header user info.
- Không -> Redirect về
/loginngay lập tức (User không bao giờ thấy trang dashboard nháy).
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('accessToken')?.value;
const isAuthPage = request.nextUrl.pathname.startsWith('/login');
// Case 1: Đã login mà cố vào trang login -> Đá về home
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/', request.url));
}
// Case 2: Chưa login mà vào trang Private -> Đá về login
if (!isAuthPage && !token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};5. Giải pháp 4: React Server Components (RSC) trong App Router
Trong Next.js 13+, bạn có thể check auth ngay trong Server Component.
// app/layout.tsx (Server Component)
import { cookies } from 'next/headers';
export default async function RootLayout({ children }) {
const cookieStore = cookies();
const token = cookieStore.get('accessToken');
// Fetch user từ API ngay trên server (Rất nhanh do cùng mạng nội bộ)
const user = token ? await getUserFromAPI(token.value) : null;
return (
<html>
<body>
{/* Truyền user đã fetch sẵn xuống Client */}
<Header initialUser={user} />
{children}
</body>
</html>
);
}Kết quả: HTML trả về cho browser là HTML ĐÚNG (đã có avatar). Không hề có sự thay đổi nào ở Client -> Zero Flicker.
Tóm tắt chiến lược
| Môi trường | Giải pháp tốt nhất | Độ khó | Hiệu quả |
|---|---|---|---|
| React SPA (Vite) | Loading Skeleton | Dễ | Tốt |
| Next.js Pages | getServerSideProps (cũ) | Trung bình | Tốt (nhưng chậm TTFB) |
| Next.js App Router | Server Components + Middleware | Cao | Tuyệt đối (Zero Flicker) |
Lời khuyên cuối
Nếu bạn đang dùng Next.js App Router:
- Dùng Middleware để bảo vệ các route private (tránh flash nội dung nhạy cảm).
- Dùng Server Component (
layout.tsx) để lấy session user và truyền xuống Header. Đây là cách chuẩn chỉ nhất hiện nay.