Layout, Template, Loading, Error

Bài 5 – Tổ chức layout, template, loading và error trong App Router

18/11/2025 DaiPhan
Bài 5 / 18

Layout, Template, Loading UI và Error UI

App Router của Next.js cung cấp hệ thống layout mạnh mẽ, giúp xây dựng cấu trúc UI ổn định, hạn chế re-render và giữ nguyên trạng thái khi chuyển trang. Ngoài ra, Next.js hỗ trợ loading UI tự động và cơ chế bắt lỗi theo từng route.


1. Layout trong Next.js

1.1. Layout là gì?

Layout là component bọc xung quanh các page con, giúp:

  • Giữ nguyên UI khi chuyển trang (header, sidebar, footer)
  • Giữ state và không bị re-render
  • Tổ chức code theo nhóm chức năng

1.2. Root Layout (Bắt buộc)

// app/layout.tsx – Layout gốc cho toàn bộ app
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="vi">
      <body>
        <header>
          <nav>
            <a href="/">Trang chủ</a>
            <a href="/about">Giới thiệu</a>
            <a href="/dashboard">Dashboard</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>&copy; 2024 My App. All rights reserved.</p>
        </footer>
      </body>
    </html>
  );
}

1.3. Nested Layouts

// app/dashboard/layout.tsx – Layout cho dashboard section
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-container">
      <aside className="sidebar">
        <h2>Dashboard</h2>
        <nav>
          <a href="/dashboard/analytics">Analytics</a>
          <a href="/dashboard/users">Users</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
      </aside>
      <section className="dashboard-content">
        {children}
      </section>
    </div>
  );
}

// app/dashboard/analytics/page.tsx
export default function AnalyticsPage() {
  return (
    <div>
      <h1>Analytics Dashboard</h1>
      <p>Welcome to analytics section!</p>
    </div>
  );
}

2. Template - Khác biệt với Layout

2.1. Template là gì?

  • Tạo mới mỗi lần chuyển route
  • Không giữ state giữa các route
  • Re-render khi chuyển trang

2.2. Khi nào dùng Template?

// app/products/template.tsx
export default function ProductsTemplate({
  children,
}: {
  children: React.ReactNode
}) {
  // Reset state mỗi lần vào products section
  const [filter, setFilter] = useState('');
  
  return (
    <div>
      <div className="filter-bar">
        <input 
          type="text" 
          placeholder="Filter products..."
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
        />
      </div>
      {children}
    </div>
  );
}

2.3. So sánh Layout vs Template

FeatureLayoutTemplate
Giữ state
Re-render
Tạo instance mới
PerformanceTốt hơnKém hơn

3. Loading UI

3.1. Loading.tsx hoạt động tự động

// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="loading-container">
      <div className="skeleton-grid">
        {[...Array(6)].map((_, i) => (
          <div key={i} className="skeleton-card">
            <div className="skeleton-image"></div>
            <div className="skeleton-text"></div>
            <div className="skeleton-text short"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

3.2. Loading với Suspense (Advanced)

// app/products/page.tsx
import { Suspense } from 'react'

export default function ProductsPage() {
  return (
    <div>
      <h1>Products</h1>
      <Suspense fallback={<ProductsLoading />}>
        <ProductsList />
      </Suspense>
    </div>
  );
}

async function ProductsList() {
  const products = await fetchProducts();
  
  return (
    <div className="products-grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

4. Error UI và Error Handling

4.1. Error.tsx - Client Component

// app/products/error.tsx
"use client"

import { useEffect } from 'react'

export default function ProductsError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log error to external service
    console.error('Products error:', error)
  }, [error])

  return (
    <div className="error-container">
      <div className="error-content">
        <h2>😔 Có lỗi xảy ra!</h2>
        <p>Không thể tải danh sách sản phẩm.</p>
        <p className="error-message">{error.message}</p>
        <div className="error-actions">
          <button onClick={reset} className="retry-button">
            🔄 Thử lại
          </button>
          <a href="/" className="home-link">
            🏠 Về trang chủ
          </a>
        </div>
      </div>
    </div>
  );
}

4.2. Global Error

// app/global-error.tsx
"use client"

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="global-error">
          <h1>🚨 Lỗi nghiêm trọng!</h1>
          <p>Ứng dụng gặp lỗi không thể khôi phục.</p>
          <button onClick={reset}>Khởi động lại</button>
        </div>
      </body>
    </html>
  );
}

5. Advanced Patterns

5.1. Layout với Metadata

// app/blog/layout.tsx
export const metadata = {
  title: 'Blog - My Website',
  description: 'Read our latest blog posts and articles',
}

export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="blog-layout">
      <header className="blog-header">
        <h1>📚 Blog</h1>
        <p>Articles, tutorials, and insights</p>
      </header>
      <div className="blog-content">
        {children}
      </div>
    </div>
  );
}

5.2. Conditional Layout

// app/(auth)/layout.tsx
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="auth-layout">
      <div className="auth-container">
        <div className="auth-form">
          {children}
        </div>
        <div className="auth-info">
          <h2>Welcome back!</h2>
          <p>Sign in to continue</p>
        </div>
      </div>
    </div>
  );
}

6. Performance Optimization

6.1. Layout Performance Tips

// ✅ Tốt: Memoize expensive components
const ExpensiveSidebar = memo(function ExpensiveSidebar() {
  return <div>{/* expensive content */}</div>
})

// ✅ Tốt: Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'))

// ❌ Tránh: Expensive operations in layout
export default function Layout({ children }) {
  const data = expensiveOperation() // Bad!
  return <div>{data}</div>
}

6.2. Loading Performance

// app/products/loading.tsx
export default function Loading() {
  return (
    <div className="loading-shimmer">
      {/* CSS animation for shimmer effect */}
      <style jsx>{`
        .loading-shimmer {
          background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
          background-size: 200% 100%;
          animation: shimmer 1.5s infinite;
        }
        @keyframes shimmer {
          0% { background-position: -200% 0; }
          100% { background-position: 200% 0; }
        }
      `}</style>
      Loading...
    </div>
  );
}

7. Bài tập thực hành

Bài tập 1: Blog Layout

Tạo layout cho /blog với:

  • Sidebar chứa danh mục bài viết
  • Header với search bar
  • Loading skeleton cho danh sách bài viết
  • Error boundary cho fetch errors

Bài tập 2: Dashboard System

Tạo dashboard layout với:

  • Top navigation bar
  • Side navigation menu
  • User profile dropdown
  • Loading states cho từng section
  • Error handling cho API calls

Bài tập 3: E-commerce Layout

Tạo e-commerce layout với:

  • Header với shopping cart
  • Product grid layout
  • Loading states cho products
  • Error handling cho cart operations

8. Kết luận bài học

  • Layout: Giữ nguyên UI và state khi chuyển trang
  • Template: Tạo mới mỗi lần chuyển route
  • Loading: UI tự động hiển thị khi đang fetch data
  • Error: Xử lý lỗi theo từng route với khả năng phục hồi

🔑 GHI NHỚ QUAN TRỌNG:

  • Luôn tạo layout phù hợp với từng section
  • Dùng loading.tsx để cải thiện UX
  • Error.tsx phải là client component
  • Plan cấu trúc layout trước khi code
18 bài học
Bài 5
Tiến độ hoàn thành 28%

Đã hoàn thành 5/18 bài học