nextjsreactmigraciónfrontend

Next.js App Router: Guía de Migración desde Pages Router

Por Binary Core

La migración de Pages Router a App Router en Next.js puede parecer intimidante, pero con una estrategia gradual es totalmente manejable. En Binary Core, hemos migrado varios proyectos exitosamente. Aquí compartimos nuestro enfoque.

Diferencias Fundamentales

Estructura de Directorios

bash
# Pages Router (antiguo) pages/ index.tsx about.tsx api/ users.ts # App Router (nuevo) app/ layout.tsx page.tsx about/ page.tsx api/ users/ route.ts

Server Components vs Client Components

typescript
// Pages Router: todo es Client Component por defecto // pages/index.tsx import { useState } from 'react'; export default function Home() { const [count, setCount] = useState(0); return <div>{count}</div>; } // App Router: Server Components por defecto // app/page.tsx export default function Home() { // ❌ Error: useState no funciona en Server Components // const [count, setCount] = useState(0); return <div>Hola desde el servidor</div>; } // Necesitas "use client" para hooks // app/components/Counter.tsx 'use client'; import { useState } from 'react'; export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }

Estrategia de Migración Gradual

Paso 1: Habilitar App Router sin romper Pages Router

Next.js permite usar ambos routers simultáneamente:

bash
# Estructura híbrida app/ layout.tsx # Layout raíz para App Router page.tsx # Home nueva pages/ _app.tsx # App antiguo (sigue funcionando) index.tsx # Home antigua (se ignora si app/page.tsx existe) about.tsx # Sigue funcionando
typescript
// app/layout.tsx import './globals.css'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="es"> <body>{children}</body> </html> ); }

Paso 2: Migrar rutas una por una

Comienza con rutas simples sin estado:

typescript
// pages/about.tsx (antiguo) export default function About() { return <div>Sobre nosotros</div>; } // app/about/page.tsx (nuevo) export default function About() { return <div>Sobre nosotros</div>; }

Paso 3: Migrar layouts

Los layouts anidados son una de las features más potentes del App Router:

typescript
// app/layout.tsx (layout raíz) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> ); } // app/dashboard/layout.tsx (layout anidado) export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <div className="dashboard"> <Sidebar /> <main>{children}</main> </div> ); } // app/dashboard/page.tsx export default function DashboardPage() { return <div>Dashboard content</div>; // Renderiza con layout raíz + layout dashboard }

Patrones que Cambian

Data Fetching

typescript
// Pages Router: getServerSideProps // pages/users/[id].tsx export async function getServerSideProps({ params }: { params: { id: string } }) { const user = await fetchUser(params.id); return { props: { user } }; } export default function UserPage({ user }: { user: User }) { return <div>{user.name}</div>; } // App Router: async Server Components // app/users/[id]/page.tsx export default async function UserPage({ params }: { params: { id: string } }) { const user = await fetchUser(params.id); return <div>{user.name}</div>; }

API Routes

typescript
// Pages Router // pages/api/users.ts export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'GET') { const users = await getUsers(); res.json(users); } } // App Router // app/api/users/route.ts import { NextResponse } from 'next/server'; export async function GET() { const users = await getUsers(); return NextResponse.json(users); } export async function POST(req: Request) { const body = await req.json(); const user = await createUser(body); return NextResponse.json(user, { status: 201 }); }

Dynamic Routes

typescript
// Pages Router // pages/blog/[slug].tsx export async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: false, }; } // App Router // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map(post => ({ slug: post.slug })); }

Errores Comunes y Soluciones

Error: useState en Server Component

typescript
// ❌ Error export default function Page() { const [state, setState] = useState(0); return <div>{state}</div>; } // ✅ Solución: extraer a Client Component // app/components/Counter.tsx 'use client'; export function Counter() { const [state, setState] = useState(0); return <div>{state}</div>; } // app/page.tsx import { Counter } from './components/Counter'; export default function Page() { return <Counter />; }

Error: Context en Server Component

typescript
// ❌ Error: useContext no funciona en Server Components import { ThemeContext } from './theme-context'; export default function Page() { const theme = useContext(ThemeContext); return <div>{theme}</div>; } // ✅ Solución: Provider en layout, consumer en Client Component // app/layout.tsx import { ThemeProvider } from './theme-provider'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <ThemeProvider>{children}</ThemeProvider> </body> </html> ); } // app/components/ThemeToggle.tsx 'use client'; import { useContext } from 'react'; import { ThemeContext } from './theme-context'; export function ThemeToggle() { const theme = useContext(ThemeContext); return <button>{theme}</button>; }

Checklist de Migración

  • Crear app/layout.tsx con estructura básica
  • Migrar rutas estáticas simples primero
  • Convertir getServerSideProps a async Server Components
  • Migrar API routes a app/api/*/route.ts
  • Extraer lógica de cliente a componentes con "use client"
  • Implementar layouts anidados donde tenga sentido
  • Migrar getStaticPaths a generateStaticParams
  • Actualizar imports de next/router a next/navigation
  • Probar cada ruta antes de eliminar versión antigua
  • Eliminar directorio pages/ cuando todo esté migrado

Conclusión

La migración a App Router no tiene que ser un evento traumático. Con una estrategia gradual, puedes ir migrando rutas una por una mientras mantienes tu aplicación funcionando. Los beneficios — Server Components por defecto, layouts anidados, mejor performance — valen el esfuerzo.

En Binary Core, hemos visto que la clave es empezar con rutas simples y no intentar migrar todo de golpe. Tómate tu tiempo, prueba cada cambio, y verás que la transición es más suave de lo que parece.

Binary Core

Equipo Binary Core

← Volver al blog