Next.js 14 App Router Migration: Troubleshooting Common Pitfalls
A comprehensive guide to resolving the most common issues encountered when migrating from Pages Router to App Router in Next.js 14
Next.js 14 App Router Migration: Troubleshooting Common Pitfalls
Introduction
Migrating from the Pages Router to the App Router in Next.js 14 is one of the most significant changes a Next.js developer will face. When I decided to migrate a production project in Lisbon, I quickly discovered that the official migration guide barely scratches the surface of what can go wrong. After spending three weeks debugging edge cases, I compiled the most common pitfalls and their solutions into this post.
The App Router brings React Server Components, nested layouts, and a new data-fetching paradigm, but these come with breaking changes that can silently break your application if you are not careful.
Environment
- OS: Windows 11
- Node.js: v20.10.0
- Next.js: 14.0.4
- React: 18.2.0
- Package Manager: pnpm 8.12.0
Problem
After running the initial migration steps from the Next.js documentation, the application compiled but threw runtime errors on several pages. The most common errors were:
Error: Objects are not valid as a React child
Error: Cannot read properties of undefined (reading 'headers')
Error: A client component can not be rendered inside a server component
Warning: Extra attributes from the server: data-newAdditionally, several API routes that previously worked under pages/api were now returning 404 errors, and the static generation of pages was failing silently.
Analysis
After thorough investigation, I identified five distinct root causes:
1. Mixing Server and Client Components incorrectly. The App Router renders everything as a Server Component by default. Any component using useState, useEffect, or browser APIs must be explicitly marked with "use client".
2. The getServerSideProps to Server Component transition. Many developers copy-paste their getServerSideProps logic directly into page components without adapting it to the new async Server Component model.
3. Missing root layout. The App Router requires a layout.tsx in the app directory. Without it, Next.js throws cryptic errors.
4. Data fetching location changes. In Pages Router, data fetching happened at the page level. In App Router, it can happen at any component level, which means fetching logic needs restructuring.
5. API route migration. API routes must move from pages/api to app/api. While Next.js provides some compatibility, it is not perfect.
Solution
Here is the step-by-step approach I used to resolve each issue:
Step 1: Create the root layout
// app/layout.tsx
export const metadata = {
title: 'My Application',
description: 'My application description',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{children}
)
}Step 2: Mark client components properly
// components/InteractiveWidget.tsx
'use client'
import { useState } from 'react'
export function InteractiveWidget() {
const [count, setCount] = useState(0)
return (
)
}Step 3: Adapt data fetching to Server Components
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // or use revalidate option
})
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
{posts.map((post: any) => (
- {post.title}
))}
)
}Step 4: Migrate API routes
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await fetchPosts()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const newPost = await createPost(body)
return NextResponse.json(newPost, { status: 201 })
}Step 5: Handle headers() and cookies() in Server Components
// app/dashboard/page.tsx
import { headers, cookies } from 'next/headers'
export default async function Dashboard() {
const headersList = headers()
const cookieStore = cookies()
const userAgent = headersList.get('user-agent')
const session = cookieStore.get('session')
return Dashboard for user: {session?.value}
}Lessons Learned
Migrate incrementally. Do not attempt to migrate the entire application at once. Use
next.config.jsto keep both routers running simultaneously with theappdirectory alongsidepages.Understand the mental model shift. Server Components are the default. You must actively opt into client-side interactivity. This is the opposite of the Pages Router mindset.
Test every route after migration. Automated testing is critical. I recommend adding Playwright tests for every route before beginning migration.
Watch for hydration mismatches. Server-rendered HTML that differs from client-rendered HTML causes hydration errors. Use
useEffectto defer client-only rendering.Keep the old
pagesdirectory during transition. Next.js supports both routers simultaneously, so migrate page by page rather than doing a big-bang switch.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.