Next.js Curriculum
MediumPages Router Migration100% Free25 min read

Custom _app.js and _document.js Lifecycle

The 30-Second Interview Answer

In Pages Router, _document.js rendered strictly on the server to customize the initial HTML shell (<html>, <head>, <body>), but could not contain client logic. _app.js initialized every page on both server and client, serving as the central hub for global CSS, layout persistence, and global providers. In App Router, both are replaced by app/layout.tsx.

Learning Objectives

Differentiate the execution environments of _app.js versus _document.js.

Explain why event handlers and client state were prohibited in _document.js.

Map global providers from _app.js into App Router layout.tsx and Client Providers.

Understand how App Router eliminates the need for two separate root configuration files.

Prerequisites & Background Knowledge
HTML document structureReact tree mountingPages Router conventions

Why Interviewers Care

Crucial for understanding why legacy codebases are structured the way they are and how to refactor them cleanly into App Router.

The Challenge

Compare the execution lifecycles of _app.js and _document.js in Pages Router. How does Next.js App Router unify these responsibilities into app/layout.tsx?

Code Architecture & Implementation

1// pages/_document.tsx (SERVER-ONLY: Renders static HTML skeleton)
2import { Html, Head, Main, NextScript } from 'next/document';
3
4export default function Document() {
5 return (
6 <Html lang="en">
7 <Head>
8 {/* Only static tags allowed here; no onClick or client state! */}
9 </Head>
10 <body className="antialiased">
11 <Main />
12 <NextScript />
13 </body>
14 </Html>
15 );
16}
17
18// ─────────────────────────────────────────────────────────────
19
20// pages/_app.tsx (SERVER & CLIENT: Wraps every page transition)
21import type { AppProps } from 'next/app';
22import '@/styles/globals.css';
23import { ThemeProvider } from '@/context/ThemeContext';
24
25export default function MyApp({ Component, pageProps }: AppProps) {
26 return (
27 <ThemeProvider>
28 {/* Re-renders on every page change; state can be reset without getLayout */}
29 <Component {...pageProps} />
30 </ThemeProvider>
31 );
32}

Architectural Explanation

Pages Router split document structure into two files because React historically lacked streaming Server Components. _document.js ran once on the server to emit <Html>, <Head>, and scripts. _app.js executed on both server and client to mount providers. In App Router, app/layout.tsx is a Server Component that renders the <html> and <body> tags directly. It remains mounted during client-side route transitions, avoiding re-mounting costs and unifying the setup into a single file.

Common Mistakes & Anti-Patterns

Adding React state (`useState`) or `onClick` handlers inside `_document.tsx`, which is ignored or throws errors because it never hydrates.

Adding `getInitialProps` to `_app.tsx`, accidentally turning all static pages into SSR pages.

Failing to isolate Client Providers in App Router, causing the root layout to become a Client Component.

Interview Follow-Up Traps

The interviewer asks: "Why did navigating between pages in Pages Router re-render the layout?" Because in Pages Router, `Component` in `_app.tsx` swapped out completely on navigation unless the custom `getLayout` pattern was manually wired up.

Frequently Asked Questions

How do you inject custom scripts in Pages Router vs App Router?

In Pages Router, use `next/script` in `_app.js`. In App Router, use `next/script` in `layout.tsx`.

Yes, Pages Router required custom `ServerStyleSheet` collection in `_document.js.getInitialProps`.

Nested layouts (`app/dashboard/layout.tsx`) handle layout persistence automatically without code.