# 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 ( } /> } /> } /> ); } ``` #### 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 ( ); } ``` #### New: Next.js Links ```typescript import Link from 'next/link'; import { useRouter } from 'next/navigation'; function Navigation() { const router = useRouter(); return ( ); } ``` ### 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 (

Theme Parks

{parks.results.map(park => ( ))}
); } ``` ### 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 ( ); } ``` ### 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 (

Parks

{/* Client Component for interactivity */} {/* Server Component for display */}
); } ``` --- ## 📡 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
Loading...
; return
{park.name}
; } ``` ### 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
{park.name}
; } ``` ### 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 (
); } ``` --- ## 🎨 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 (
{children}