Mục lục

Authentication UX: Chống nháy Layout (FOUC)

Kỹ thuật xử lý Authentication State trong Next.js/React để tránh hiện tượng giao diện bị nháy (Flash of Unauthenticated Content) khi reload trang.

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:

  1. 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".
  2. JS chạy ở Client: Gọi API /me hoặc đọc LocalStorage.
  3. 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 đó.


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.

tsx:
// 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.

tsx:
// 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:

  1. User request /dashboard.
  2. Middleware check Cookie accessToken.
    • Có -> Cho qua, có thể inject header user info.
    • Không -> Redirect về /login ngay lập tức (User không bao giờ thấy trang dashboard nháy).
typescript:
// 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.

tsx:
// 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ườngGiải pháp tốt nhấtĐộ khóHiệu quả
React SPA (Vite)Loading SkeletonDễTốt
Next.js PagesgetServerSideProps (cũ)Trung bìnhTốt (nhưng chậm TTFB)
Next.js App RouterServer Components + MiddlewareCaoTuyệt đối (Zero Flicker)

Lời khuyên cuối

Nếu bạn đang dùng Next.js App Router:

  1. Dùng Middleware để bảo vệ các route private (tránh flash nội dung nhạy cảm).
  2. 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.
Quảng cáo
mdhorizontal