mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 15:11:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
605
migration/NEXTJS_15_MIGRATION_GUIDE.md
Normal file
605
migration/NEXTJS_15_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,605 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user