- Next.js
- Pages Router & Migration Strategy
- Custom _app.js and _document.js Lifecycle
Custom _app.js and _document.js Lifecycle
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
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';34export 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}1718// ─────────────────────────────────────────────────────────────1920// pages/_app.tsx (SERVER & CLIENT: Wraps every page transition)21import type { AppProps } from 'next/app';22import '@/styles/globals.css';23import { ThemeProvider } from '@/context/ThemeContext';2425export 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
Can you use styled-components with _document.js?
Yes, Pages Router required custom `ServerStyleSheet` collection in `_document.js.getInitialProps`.
What is the App Router equivalent of getLayout?
Nested layouts (`app/dashboard/layout.tsx`) handle layout persistence automatically without code.
