Next.js App Router
| App Router adalah sistem routing terbaru di [[NextJS | Next.js]] yang diperkenalkan di versi 13, menggantikan [[Pages Router | Pages Router]]. App Router dibangun di atas [[RSC | React Server Components]] dan menyediakan fitur-fitur modern untuk aplikasi web dengan dukungan [[SSR]] dan [[SSG]] yang lebih baik. |
Apa itu App Router?
App Router menggunakan direktori app/ untuk mendefinisikan routes dan layouts. Sistem ini mendukung:
- React Server Components by default
- Nested layouts dan nested routing
- Streaming dan Suspense
- Built-in SEO support
- Advanced routing patterns
Struktur Dasar App Router
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/[slug]
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # /dashboard
├── settings/
│ └── page.tsx # /dashboard/settings
└── analytics/
└── page.tsx # /dashboard/analytics
File Conventions
App Router menggunakan file khusus dengan fungsi tertentu:
1. layout.tsx
// app/layout.tsx - Root Layout (Required)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>Global Header</header>
<main>{children}</main>
<footer>Global Footer</footer>
</body>
</html>
)
}
2. page.tsx
// app/page.tsx - Home Page
export default function HomePage() {
return (
<div>
<h1>Welcome to Next.js App Router</h1>
<p>This is the home page</p>
</div>
)
}
// app/about/page.tsx - About Page
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
)
}
3. loading.tsx
// app/loading.tsx - Global Loading UI
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
)
}
// app/dashboard/loading.tsx - Dashboard Loading UI
export default function DashboardLoading() {
return (
<div className="dashboard-loading">
<div className="skeleton-header"></div>
<div className="skeleton-content"></div>
</div>
)
}
4. error.tsx
// app/error.tsx - Global Error UI
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>
Try again
</button>
</div>
)
}
5. not-found.tsx
// app/not-found.tsx - 404 Page
import Link from 'next/link'
export default function NotFound() {
return (
<div className="not-found">
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}
Dynamic Routes
1. Single Dynamic Segment
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
</article>
)
}
// Generates routes like:
// /blog/hello-world
// /blog/nextjs-tutorial
2. Catch-all Routes
// app/shop/[...slug]/page.tsx
export default function ShopPage({ params }: { params: { slug: string[] } }) {
return (
<div>
<h1>Shop</h1>
<p>Category path: {params.slug.join('/')}</p>
</div>
)
}
// Generates routes like:
// /shop/clothing
// /shop/clothing/shirts
// /shop/clothing/shirts/casual
3. Optional Catch-all Routes
// app/docs/[[...slug]]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
if (!params.slug) {
return <div>Documentation Home</div>
}
return (
<div>
<h1>Docs: {params.slug.join('/')}</h1>
</div>
)
}
// Matches:
// /docs (slug is undefined)
// /docs/getting-started
// /docs/api/reference
Nested Layouts
App Router memungkinkan nested layouts yang powerful:
// app/layout.tsx - Root Layout
export default function RootLayout({ children }) {
return (
<html>
<body>
<nav>Global Navigation</nav>
{children}
</body>
</html>
)
}
// app/dashboard/layout.tsx - Dashboard Layout
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<aside>
<nav>Dashboard Navigation</nav>
</aside>
<main>{children}</main>
</div>
)
}
// app/dashboard/settings/layout.tsx - Settings Layout
export default function SettingsLayout({ children }) {
return (
<div className="settings">
<div className="settings-sidebar">
<nav>Settings Navigation</nav>
</div>
<div className="settings-content">
{children}
</div>
</div>
)
}
Server Components vs Client Components
Server Components (Default)
// app/posts/page.tsx - Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Client Components
// app/components/SearchBox.tsx - Client Component
'use client'
import { useState } from 'react'
export default function SearchBox() {
const [query, setQuery] = useState('')
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button onClick={() => console.log('Searching:', query)}>
Search
</button>
</div>
)
}
Data Fetching
1. Server-side Data Fetching
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
cache: 'force-cache' // Static generation
})
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
2. Dynamic Data Fetching
// app/dashboard/page.tsx
async function getDashboardData() {
const res = await fetch('https://api.example.com/dashboard', {
cache: 'no-store' // Always fresh data
})
return res.json()
}
export default async function DashboardPage() {
const data = await getDashboardData()
return (
<div>
<h1>Dashboard</h1>
<p>Last updated: {data.lastUpdated}</p>
</div>
)
}
3. Revalidated Data Fetching
// app/news/page.tsx
async function getNews() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 300 } // Revalidate every 5 minutes
})
return res.json()
}
export default async function NewsPage() {
const news = await getNews()
return (
<div>
{news.map(article => (
<article key={article.id}>
<h2>{article.title}</h2>
</article>
))}
</div>
)
}
Route Groups
Route groups memungkinkan organisasi routes tanpa mempengaruhi URL:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
├── (shop)/
│ ├── products/
│ │ └── page.tsx # /products
│ └── cart/
│ └── page.tsx # /cart
└── (auth)/
├── login/
│ └── page.tsx # /login
└── register/
└── page.tsx # /register
Multiple Root Layouts
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
return (
<div className="marketing-layout">
<nav>Marketing Navigation</nav>
{children}
</div>
)
}
// app/(shop)/layout.tsx
export default function ShopLayout({ children }) {
return (
<div className="shop-layout">
<nav>Shop Navigation</nav>
{children}
</div>
)
}
Parallel Routes
Parallel routes memungkinkan rendering multiple pages dalam layout yang sama:
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── @dashboard/
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
dashboard,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
dashboard: React.ReactNode
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
<div>{analytics}</div>
<div>{team}</div>
</div>
<div>{dashboard}</div>
</div>
)
}
Intercepting Routes
Intercepting routes memungkinkan loading route dalam context current page:
app/
├── feed/
│ ├── page.tsx
│ └── (..)photo/
│ └── [id]/
│ └── page.tsx
└── photo/
└── [id]/
└── page.tsx
// app/feed/(..)photo/[id]/page.tsx - Modal view
export default function PhotoModal({ params }) {
return (
<div className="modal">
<div className="modal-content">
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</div>
</div>
)
}
// app/photo/[id]/page.tsx - Full page view
export default function PhotoPage({ params }) {
return (
<div className="photo-page">
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</div>
)
}
Metadata API
App Router menyediakan API untuk SEO metadata:
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Migration dari Pages Router
1. Struktur File
// Pages Router
pages/
├── index.tsx → app/page.tsx
├── about.tsx → app/about/page.tsx
├── blog/
│ ├── index.tsx → app/blog/page.tsx
│ └── [slug].tsx → app/blog/[slug]/page.tsx
└── _app.tsx → app/layout.tsx
2. Data Fetching
// Pages Router
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
// App Router
async function getData() {
const res = await fetch('...')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data}</div>
}
Best Practices
1. Component Organization
app/
├── components/ # Shared components
│ ├── ui/ # Basic UI components
│ └── features/ # Feature components
├── lib/ # Utilities
├── hooks/ # Custom hooks
└── types/ # TypeScript types
2. Error Handling
// app/dashboard/error.tsx
'use client'
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="error-container">
<h2>Dashboard Error</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
3. Loading States
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded mb-4"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
)
}
Kesimpulan
App Router adalah evolusi besar dalam Next.js yang membawa:
- Better developer experience dengan file conventions
- Improved performance dengan Server Components
- Enhanced SEO dengan built-in metadata API
- More flexible routing dengan nested layouts
Migrasi ke App Router direkomendasikan untuk:
- Proyek baru
- Aplikasi yang butuh SEO optimal
- Tim yang ingin leverage React Server Components
- Aplikasi dengan complex routing needs
Related Notes:
-
[[NextJS Next.js]] - Framework utama -
[[Pages Router Pages Router]] - Sistem routing lama -
[[RSC React Server Components]] - Komponen server React -
[[SSR Server Side Rendering]] - Rendering strategy -
[[SSG Static Site Generation]] - Static generation -
[[React Framework Framework React]] - Konsep framework React -
[[Web Performance Performance]] - Optimasi performa -
[[SEO Optimization SEO]] - Search engine optimization
External Links:
- [[App Router Documentation::https://nextjs.org/docs/app]]
- [[React Server Components::https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components]]
- [[Next.js Migration Guide::https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration]]