Bài 28.4: Custom App và Document NextJS

Chào mừng trở lại với series lập trình web! Hôm nay, chúng ta sẽ đi sâu vào hai file đặc biệt trong Next.js giúp bạn kiểm soát toàn bộ ứng dụng của mình từ gốc: _app.js (hoặc _app.tsx) và _document.js (hoặc _document.tsx). Đây là những file không bắt buộc phải có, nhưng khi bạn cần tùy chỉnh sâu hơn, chúng trở nên vô cùng mạnh mẽ.

Hãy coi _app.js như trái tim của ứng dụng React của bạn, còn _document.jsbộ khung HTML bao bọc lấy trái tim đó.

Hiểu về _app.js (hoặc _app.tsx)

Khi người dùng truy cập vào một trang trong ứng dụng Next.js của bạn, Next.js sẽ sử dụng file _app.js làm thành phần gốc (root component) để bao bọc lấy thành phần trang (Page component) tương ứng. Nghĩa là, mọi trang (pages/index.js, pages/about.js, v.v.) sẽ được render bên trong _app.js.

Vị trí: Bạn cần tạo file này trong thư mục pages/ của project. Nếu nó chưa tồn tại, Next.js sẽ sử dụng một _app.js mặc định.

Tại sao lại cần tùy chỉnh _app.js?

  • Import CSS toàn cục (Global CSS): Next.js chỉ cho phép import CSS toàn cục (global) trong file _app.js.
  • Duy trì trạng thái (State) giữa các trang: Nếu bạn sử dụng Redux, Context API, hoặc các thư viện quản lý trạng thái khác, bạn sẽ thiết lập Provider ở đây để trạng thái được chia sẻ trên toàn bộ ứng dụng.
  • Inject dữ liệu vào trang (Inject data into pages): Bạn có thể thêm các props tùy chỉnh vào pageProps được truyền xuống các Page component.
  • Persistent Layouts: Nếu bạn có một layout chung (ví dụ: header, footer, sidebar) mà không muốn bị re-render mỗi khi chuyển trang, _app.js là nơi để xử lý logic này.

Hãy xem cấu trúc cơ bản của một file _app.js:

// pages/_app.js

import '../styles/globals.css'; // <-- Nơi duy nhất để import global CSS

function MyApp({ Component, pageProps }) {
  // Logic chung cho toàn bộ ứng dụng
  // Ví dụ: xác thực người dùng, thiết lập provider

  // 'Component' là Page component hiện tại đang được render
  // 'pageProps' là các props được trả về từ getStaticProps hoặc getServerSideProps của trang đó
  return <Component {...pageProps} />;
}

export default MyApp;

Trong đoạn code trên:

  • import '../styles/globals.css';: Đây là cách bạn áp dụng các style toàn cục cho toàn bộ ứng dụng.
  • MyApp: Tên của component gốc tùy chỉnh của bạn. Bạn có thể đặt tên khác, nhưng MyApp là quy ước phổ biến.
  • { Component, pageProps }: Đây là các props Next.js truyền xuống. Component chính là Page component hiện tại (Home, About, etc.), còn pageProps chứa dữ liệu mà các hàm fetching data (như getStaticProps, getServerSideProps) của trang đó trả về.

Ví dụ 1: Thêm Global Styles

Như đã thấy ở trên, chỉ cần import file CSS toàn cục vào _app.js là đủ.

// pages/_app.js
import '../styles/globals.css'; // File này chứa các style áp dụng cho body, h1, etc.

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}
export default MyApp;

Giải thích: Next.js giới hạn việc import CSS toàn cục để tránh xung đột style. File globals.css (hoặc tên bất kỳ) chỉ có thể được import duy nhất tại _app.js.

Ví dụ 2: Thiết lập Context Provider

Nếu bạn sử dụng React Context API để quản lý trạng thái, bạn sẽ bọc toàn bộ ứng dụng bằng Provider ở đây.

// pages/_app.js
import '../styles/globals.css';
import { AuthProvider } from '../context/AuthContext'; // Giả sử bạn có AuthContext

function MyApp({ Component, pageProps }) {
  return (
    <AuthProvider> {/* Bọc toàn bộ ứng dụng bằng AuthProvider */}
      <Component {...pageProps} />
    </AuthProvider>
  );
}

export default MyApp;

Giải thích: Bằng cách đặt AuthProvider ở đây, bất kỳ Page component nào hoặc component con nào của nó đều có thể truy cập vào Context AuthContext bằng hook useContext. Điều này đảm bảo trạng thái xác thực được chia sẻ trên toàn ứng dụng.

Hiểu về _document.js (hoặc _document.tsx)

Trong khi _app.js điều khiển React component tree của ứng dụng, _document.js cho phép bạn tùy chỉnh cấu trúc HTML ban đầu được render bởi server. Nghĩa là, bạn có thể thay đổi các thẻ HTML gốc như <html>, <head> (phần không thuộc React app), và <body>.

Vị trí: Bạn cũng cần tạo file này trong thư mục pages/. Nếu nó không tồn tại, Next.js sẽ sử dụng một _document.js mặc định.

Lưu ý quan trọng: _document.js chỉ chạy trên server trong quá trình render ban đầu (initial server render). Nó không chạy trên client hoặc khi chuyển trang client-side. Do đó, bạn không thể sử dụng các hook của React (như useState, useEffect) hoặc thêm các logic xử lý sự kiện (event listeners) trong file này. Mục đích của nó là tạo ra cấu trúc HTML tĩnh ban đầu.

Tại sao lại cần tùy chỉnh _document.js?

  • Thêm thuộc tính cho <html> hoặc <body>: Ví dụ: thêm thuộc tính lang="vi" cho thẻ <html>, hoặc thêm một class cho thẻ <body>.
  • Thêm các thẻ <meta> hoặc <link> tùy chỉnh vào <head>: Những thẻ này thường là các meta tag đặc biệt, link tới font chữ bên ngoài, hoặc link tới CDN scripts mà bạn muốn tải trước khi ứng dụng React hoàn toàn chạy. Lưu ý: Đối với các thẻ <meta> hoặc <link> thay đổi theo từng trang (ví dụ: title, description cho SEO), bạn nên dùng next/head trong từng Page component thay vì ở đây.
  • Chèn các script bên ngoài NextScript: Ví dụ: mã theo dõi Google Analytics.

Cấu trúc cơ bản của _document.js trông như sau:

// pages/_document.js

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      // <Html>, <Head>, <Main>, <NextScript> là các component bắt buộc
      <Html lang="vi"> {/* Ví dụ: thêm thuộc tính lang */}
        <Head>
          {/* Các thẻ <meta>, <link> tùy chỉnh của bạn ở đây */}
          {/* KHÔNG sử dụng next/head ở đây */}
          {/* KHÔNG sử dụng hook, state, event listeners ở đây */}
          <link
            href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
            rel="stylesheet"
          />
        </Head>
        <body>
          {/* Nơi Next.js render ứng dụng React của bạn */}
          <Main />

          {/* Các script cần thiết để Next.js hoạt động */}
          <NextScript />

          {/* Có thể thêm script tùy chỉnh sau NextScript, ví dụ: analytics */}
          {/* <script dangerouslySetInnerHTML={{ __html: `window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'YOUR_GA_ID');` }} /> */}
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Trong đoạn code trên:

  • import Document, { Html, Head, Main, NextScript } from 'next/document';: Bạn cần import các thành phần này từ next/document.
  • class MyDocument extends Document: _document.js phải là một class component mở rộng từ Document.
  • render(): Phương thức này trả về cấu trúc HTML.
  • <Html>, <Head>, <Main>, <NextScript>: Đây là các component bắt buộc phải có.
    • <Html>: Thẻ <html> gốc.
    • <Head>: Phần <head> của tài liệu. Lưu ý: <Head> ở đây khác với <Head> từ next/head (sử dụng trong Page component). <Head> trong _document dành cho các thẻ cố định cho toàn bộ document, còn <Head> trong next/head dành cho các thẻ động theo từng trang.
    • <Main>: Nơi Next.js sẽ render Page component của bạn (đã được bao bọc bởi _app).
    • <NextScript>: Các script cần thiết để Next.js hoạt động (hydrating React app, routing, v.v.).
  • <link href="..." rel="stylesheet">: Ví dụ về cách thêm link font chữ trực tiếp vào <head> ban đầu.
  • lang="vi": Ví dụ về cách thêm thuộc tính cho thẻ <html>.

Ví dụ 1: Thêm thuộc tính lang và link Font

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="vi"> {/* Thiết lập ngôn ngữ của trang */}
        <Head>
          {/* Link tới Google Fonts */}
          <link
            href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap"
            rel="stylesheet"
          />
          {/* Có thể thêm các meta tag cố định khác ở đây nếu cần */}
          {/* <meta name="theme-color" content="#ffffff" /> */}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Giải thích: Việc thêm lang="vi" giúp các công cụ tìm kiếm và trình đọc màn hình hiểu ngôn ngữ chính của trang. Link tới font chữ được đặt trong <head> để trình duyệt có thể bắt đầu tải font sớm.

Ví dụ 2: Thêm Class cho <body> và Script Analytics

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="vi">
        <Head>
          {/* ... các thẻ head khác ... */}
        </Head>
        <body className="my-custom-body-class"> {/* Thêm class cho body */}
          <Main />
          <NextScript />
          {/* Thêm script analytics sau NextScript để không chặn render */}
          <script
            async
            src={`https://www.googletagmanager.com/gtag/js?id=YOUR_GA_MEASUREMENT_ID`} // Thay YOUR_GA_MEASUREMENT_ID
          />
          <script
            dangerouslySetInnerHTML={{
              __html: `
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());
                gtag('config', 'YOUR_GA_MEASUREMENT_ID', {
                  page_path: window.location.pathname,
                });
              `,
            }}
          />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Giải thích: Thêm class vào <body> có thể hữu ích cho việc áp dụng các style toàn cục dựa trên class đó. Việc đặt mã script analytics sau NextScript là một kỹ thuật phổ biến để đảm bảo các script quan trọng của Next.js được tải và thực thi trước, không làm chậm quá trình khởi động ứng dụng React. Thuộc tính dangerouslySetInnerHTML được dùng để chèn code JavaScript raw vào script tag.

_app vs _document: Khi nào dùng cái nào?

Sự khác biệt rõ ràng giữa hai file này nằm ở phạm vi và thời điểm chạy:

  • _app.js:

    • Chạy cả trên Server và Client.
    • Là một React component bao bọc toàn bộ cây component của ứng dụng.
    • Dùng cho: Global styles (import CSS), quản lý trạng thái (Providers), persistent layouts, thêm logic chung cho các trang.
    • Có thể sử dụng React Hooks, State, Event Listeners.
  • _document.js:

    • Chỉ chạy trên Server trong quá trình render ban đầu.
    • Là một class component dùng để tùy chỉnh cấu trúc HTML <html>, <head> (phần không thuộc React app), <body>.
    • Dùng cho: Thêm thuộc tính cho <html>/<body>, thêm link fonts, CDN scripts, meta tags cố định vào <head> ban đầu.
    • Không thể sử dụng React Hooks, State, Event Listeners. Chỉ trả về cấu trúc HTML tĩnh.

Tóm lại, hãy nghĩ thế này: Nếu bạn muốn ảnh hưởng đến cách ứng dụng React của bạn hoạt động (style, state, layout, logic chung), hãy dùng _app.js. Nếu bạn muốn tùy chỉnh cấu trúc HTML gốc của trang được gửi từ server (thêm font, script ngoài, thuộc tính HTML), hãy dùng _document.js.

Việc làm chủ hai file đặc biệt này giúp bạn có quyền kiểm soát mạnh mẽ và linh hoạt hơn rất nhiều đối với nền tảng của ứng dụng Next.js, cho phép bạn tối ưu hóa hiệu suất, quản lý tài nguyên hiệu quả và tích hợp các thư viện bên ngoài một cách phù hợp.

Comments

There are no comments at the moment.