Files
thrilltrack-explorer/migration/NEXTJS_15_MIGRATION_GUIDE.md

606 lines
12 KiB
Markdown

# Next.js 15 Migration Guide
**For:** ThrillWiki React → Next.js 15 + App Router Migration
**Last Updated:** November 9, 2025
---
## 🎯 Overview
This guide provides patterns, examples, and best practices for migrating from React SPA to Next.js 15 with App Router.
**What's Changing:**
- React Router → Next.js File-based Routing
- Client-side rendering → Server-side rendering by default
- Manual data fetching → Built-in data fetching
- Vite → Turbopack
- npm → Bun
---
## 📁 File Structure Migration
### Old Structure (React SPA)
```
src/
├── pages/
│ ├── Index.tsx
│ ├── Parks.tsx
│ ├── ParkDetail.tsx
│ └── ...
├── components/
│ ├── Navigation.tsx
│ ├── ParkCard.tsx
│ └── ...
├── hooks/
├── services/
└── App.tsx
```
### New Structure (Next.js 15)
```
app/
├── layout.tsx # Root layout (replaces App.tsx)
├── page.tsx # Homepage (replaces Index.tsx)
├── parks/
│ ├── page.tsx # /parks (replaces Parks.tsx)
│ └── [parkSlug]/
│ └── page.tsx # /parks/[slug] (replaces ParkDetail.tsx)
components/
├── navigation/
│ └── Navigation.tsx # Shared components
├── parks/
│ └── ParkCard.tsx
hooks/
services/
lib/
```
---
## 🔄 Routing Migration
### React Router → Next.js
#### Old: React Router
```typescript
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/parks" element={<ParksPage />} />
<Route path="/parks/:slug" element={<ParkDetailPage />} />
</Routes>
</BrowserRouter>
);
}
```
#### New: Next.js File-based Routing
```
app/
├── page.tsx → /
├── parks/
│ ├── page.tsx → /parks
│ └── [slug]/
│ └── page.tsx → /parks/:slug
```
### Navigation
#### Old: React Router Links
```typescript
import { Link, useNavigate } from 'react-router-dom';
function Navigation() {
const navigate = useNavigate();
return (
<nav>
<Link to="/parks">Parks</Link>
<button onClick={() => navigate('/parks/cedar-point')}>
Cedar Point
</button>
</nav>
);
}
```
#### New: Next.js Links
```typescript
import Link from 'next/link';
import { useRouter } from 'next/navigation';
function Navigation() {
const router = useRouter();
return (
<nav>
<Link href="/parks">Parks</Link>
<button onClick={() => router.push('/parks/cedar-point')}>
Cedar Point
</button>
</nav>
);
}
```
### URL Parameters
#### Old: React Router
```typescript
import { useParams } from 'react-router-dom';
function ParkDetail() {
const { slug } = useParams();
// ...
}
```
#### New: Next.js (Server Component)
```typescript
// app/parks/[slug]/page.tsx
export default function ParkDetail({
params
}: {
params: { slug: string }
}) {
// params.slug is available
}
```
#### New: Next.js (Client Component)
```typescript
'use client';
import { useParams } from 'next/navigation';
export default function ParkDetail() {
const params = useParams();
const slug = params.slug;
}
```
### Query Parameters
#### Old: React Router
```typescript
import { useSearchParams } from 'react-router-dom';
function ParksPage() {
const [searchParams] = useSearchParams();
const filter = searchParams.get('filter');
}
```
#### New: Next.js (Server Component)
```typescript
export default function ParksPage({
searchParams
}: {
searchParams: { filter?: string }
}) {
const filter = searchParams.filter;
}
```
#### New: Next.js (Client Component)
```typescript
'use client';
import { useSearchParams } from 'next/navigation';
export default function ParksPage() {
const searchParams = useSearchParams();
const filter = searchParams.get('filter');
}
```
---
## 🖥️ Server vs Client Components
### Decision Tree
```
Does it need interactivity?
├─ NO → Server Component (default)
│ ├─ Displays data
│ ├─ Static content
│ └─ SEO-critical
└─ YES → Client Component ('use client')
├─ Forms with state
├─ Event handlers
├─ Browser APIs
└─ Hooks (useState, useEffect, etc.)
```
### Server Component Example
```typescript
// app/parks/page.tsx
// No 'use client' directive = Server Component
import { env } from '@/lib/env';
export default async function ParksPage() {
// Fetch data directly in component
const parks = await fetch(
`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/`
).then(r => r.json());
return (
<div>
<h1>Theme Parks</h1>
{parks.results.map(park => (
<ParkCard key={park.id} park={park} />
))}
</div>
);
}
```
### Client Component Example
```typescript
// components/parks/ParkFilters.tsx
'use client'; // Mark as Client Component
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function ParkFilters() {
const [filter, setFilter] = useState('');
const router = useRouter();
const handleFilterChange = (value: string) => {
setFilter(value);
router.push(`/parks?filter=${value}`);
};
return (
<select value={filter} onChange={(e) => handleFilterChange(e.target.value)}>
<option value="">All Parks</option>
<option value="theme">Theme Parks</option>
<option value="water">Water Parks</option>
</select>
);
}
```
### Mixing Server and Client
```typescript
// app/parks/page.tsx (Server Component)
import { ParkFilters } from '@/components/parks/ParkFilters'; // Client
import { ParksList } from '@/components/parks/ParksList'; // Can be Server
export default async function ParksPage() {
const parks = await fetch(API_URL).then(r => r.json());
return (
<div>
<h1>Parks</h1>
<ParkFilters /> {/* Client Component for interactivity */}
<ParksList parks={parks} /> {/* Server Component for display */}
</div>
);
}
```
---
## 📡 Data Fetching
### Old: React with useEffect
```typescript
function ParkDetail({ slug }: { slug: string }) {
const [park, setPark] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/parks/${slug}`)
.then(r => r.json())
.then(data => {
setPark(data);
setLoading(false);
});
}, [slug]);
if (loading) return <div>Loading...</div>;
return <div>{park.name}</div>;
}
```
### New: Next.js Server Component
```typescript
// app/parks/[slug]/page.tsx
export default async function ParkDetail({
params
}: {
params: { slug: string }
}) {
// Fetch directly - no useEffect needed
const park = await fetch(
`${API_URL}/parks/${params.slug}/`,
{ next: { revalidate: 3600 } } // Cache for 1 hour
).then(r => r.json());
return <div>{park.name}</div>;
}
```
### Caching Strategies
```typescript
// No caching (always fresh)
fetch(url, { cache: 'no-store' });
// Cache forever (until revalidated)
fetch(url, { cache: 'force-cache' });
// Revalidate after 60 seconds
fetch(url, { next: { revalidate: 60 } });
// Revalidate on-demand (from API route)
fetch(url, { next: { tags: ['parks'] } });
// Then: revalidateTag('parks')
```
### Parallel Data Fetching
```typescript
export default async function ParkDetail({ params }) {
// Fetch in parallel
const [park, rides, reviews] = await Promise.all([
fetch(`${API_URL}/parks/${params.slug}/`),
fetch(`${API_URL}/parks/${params.slug}/rides/`),
fetch(`${API_URL}/parks/${params.slug}/reviews/`),
]).then(responses =>
Promise.all(responses.map(r => r.json()))
);
return (
<div>
<ParkHeader park={park} />
<RidesList rides={rides} />
<ReviewsList reviews={reviews} />
</div>
);
}
```
---
## 🎨 Layouts
### Root Layout
```typescript
// app/layout.tsx
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'ThrillWiki',
description: 'Theme Park Database',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Navigation />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
```
### Nested Layouts
```typescript
// app/parks/layout.tsx
export default function ParksLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="parks-container">
<ParksSidebar />
<div className="parks-content">
{children}
</div>
</div>
);
}
```
---
## 🔍 SEO & Metadata
### Static Metadata
```typescript
// app/parks/page.tsx
export const metadata = {
title: 'Theme Parks - ThrillWiki',
description: 'Browse theme parks from around the world',
openGraph: {
title: 'Theme Parks',
description: 'Browse theme parks',
images: ['/og-parks.png'],
},
};
```
### Dynamic Metadata
```typescript
// app/parks/[slug]/page.tsx
export async function generateMetadata({ params }) {
const park = await fetch(`${API_URL}/parks/${params.slug}/`)
.then(r => r.json());
return {
title: `${park.name} - ThrillWiki`,
description: park.description,
openGraph: {
title: park.name,
description: park.description,
images: [park.image_url],
},
};
}
```
---
## ⚡ Loading & Error States
### Loading UI
```typescript
// app/parks/loading.tsx
export default function Loading() {
return (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-grid">
{[...Array(6)].map((_, i) => (
<div key={i} className="skeleton-card" />
))}
</div>
</div>
);
}
```
### Error Boundaries
```typescript
// app/parks/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
```
### Not Found
```typescript
// app/parks/[slug]/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Park Not Found</h2>
<p>The park you're looking for doesn't exist.</p>
</div>
);
}
// In page.tsx
import { notFound } from 'next/navigation';
export default async function ParkDetail({ params }) {
const park = await fetch(`${API_URL}/parks/${params.slug}/`)
.then(r => {
if (!r.ok) throw new Error();
return r.json();
})
.catch(() => notFound());
return <div>{park.name}</div>;
}
```
---
## 🚀 Deployment
### Build Command
```bash
bun run build
```
### Environment Variables
Set in Vercel/hosting platform:
- `NEXT_PUBLIC_DJANGO_API_URL`
- `NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID`
- Server-only secrets (no NEXT_PUBLIC_ prefix)
### Vercel Deployment
```bash
# Install Vercel CLI
bun add -g vercel
# Deploy
vercel
```
---
## 📚 Additional Resources
- Next.js 15 Documentation: https://nextjs.org/docs
- App Router: https://nextjs.org/docs/app
- Server Components: https://nextjs.org/docs/app/building-your-application/rendering/server-components
- Data Fetching: https://nextjs.org/docs/app/building-your-application/data-fetching
---
## 🔗 Related Documentation
- [Phase 1: Foundation](./PHASE_01_FOUNDATION.md)
- [Phase 12: Pages Migration](./PHASE_12_PAGES_MIGRATION.md)
- [Phase 13: Optimization](./PHASE_13_NEXTJS_OPTIMIZATION.md)
- [Environment Variables](./ENVIRONMENT_VARIABLES.md)
- [Bun Guide](./BUN_GUIDE.md)
---
**Document Version:** 1.0
**Last Updated:** November 9, 2025