Next.js App Router: Panduan Lengkap untuk Developer

Pelajari fitur-fitur terbaru Next.js App Router dan cara menggunakannya untuk membangun aplikasi web modern.

25 Januari 2024
Novin Ardian Yulianto
Next.jsReactApp RouterWeb Development
Next.js App Router: Panduan Lengkap untuk Developer

Next.js App Router: Panduan Lengkap untuk Developer

Next.js App Router adalah paradigma baru dalam pengembangan aplikasi Next.js yang memperkenalkan konsep-konsep modern seperti Server Components, Streaming, dan file-based routing yang lebih powerful.

Apa itu App Router?

App Router adalah sistem routing baru di Next.js 13+ yang dibangun di atas React Server Components. Ini menggantikan Pages Router dengan pendekatan yang lebih modern dan fleksibel.

Perbedaan dengan Pages Router

| Feature | Pages Router | App Router | | ------------- | -------------------------------------- | -------------------------- | | Routing | pages/ directory | app/ directory | | Layout | _app.js dan _document.js | layout.js nested | | Data Fetching | getServerSideProps, getStaticProps | fetch() dengan caching | | Components | Client Components | Server + Client Components |

Struktur Direktori App Router

app/
├── layout.js          # Root layout
├── page.js            # Home page
├── loading.js         # Loading UI
├── error.js           # Error UI
├── not-found.js       # 404 page
├── global-error.js    # Global error UI
├── route.js           # API route
├── template.js        # Template
├── default.js         # Parallel route default
└── dashboard/
    ├── layout.js      # Nested layout
    ├── page.js        # Dashboard page
    ├── settings/
    │   └── page.js    # Settings page
    └── analytics/
        └── page.js    # Analytics page

File Conventions

Layout

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>Navigation</nav>
        </header>
        <main>{children}</main>
        <footer>Footer</footer>
      </body>
    </html>
  );
}

Page

// app/dashboard/page.js
export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome to your dashboard!</p>
    </div>
  );
}

Loading

// app/dashboard/loading.js
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>
  );
}

Error

// app/dashboard/error.js
"use client"; // Error components must be Client Components

export default function Error({ error, reset }) {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
      >
        Try again
      </button>
    </div>
  );
}

Server Components vs Client Components

Server Components (Default)

// app/posts/page.js
// This is a Server Component by default
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    cache: "force-cache", // Static generation
  });
  return res.json();
}

export default async function Posts() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Posts</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

Client Components

// app/components/counter.js
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Data Fetching

Fetch dengan Caching

// Static data fetching
async function getStaticData() {
  const res = await fetch("https://api.example.com/data", {
    cache: "force-cache", // Default behavior
  });
  return res.json();
}

// Dynamic data fetching
async function getDynamicData() {
  const res = await fetch("https://api.example.com/data", {
    cache: "no-store",
  });
  return res.json();
}

// Revalidated data fetching
async function getRevalidatedData() {
  const res = await fetch("https://api.example.com/data", {
    next: { revalidate: 60 }, // Revalidate every 60 seconds
  });
  return res.json();
}

Parallel Data Fetching

// app/dashboard/page.js
async function getUser() {
  const res = await fetch("https://api.example.com/user");
  return res.json();
}

async function getPosts() {
  const res = await fetch("https://api.example.com/posts");
  return res.json();
}

export default async function Dashboard() {
  // Parallel fetching
  const [user, posts] = await Promise.all([getUser(), getPosts()]);

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <div>
        <h2>Your Posts</h2>
        {posts.map((post) => (
          <article key={post.id}>
            <h3>{post.title}</h3>
          </article>
        ))}
      </div>
    </div>
  );
}

Dynamic Routes

Basic Dynamic Route

// app/posts/[id]/page.js
export default function Post({ params }) {
  return <h1>Post ID: {params.id}</h1>;
}

// Generate static params
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((res) =>
    res.json()
  );

  return posts.map((post) => ({
    id: post.id.toString(),
  }));
}

Catch-all Routes

// app/shop/[...slug]/page.js
export default function Shop({ params }) {
  // /shop/clothes/shirts/t-shirts
  // params.slug = ['clothes', 'shirts', 't-shirts']
  return (
    <div>
      <h1>Shop</h1>
      <p>Category: {params.slug.join(" > ")}</p>
    </div>
  );
}

Route Groups

app/
├── (marketing)/
│   ├── about/
│   │   └── page.js
│   └── contact/
│       └── page.js
├── (shop)/
│   ├── products/
│   │   └── page.js
│   └── cart/
│       └── page.js
└── layout.js

Route groups memungkinkan Anda mengorganisir routes tanpa mempengaruhi URL structure.

Parallel Routes

app/
├── @analytics/
│   └── page.js
├── @team/
│   └── page.js
├── layout.js
└── page.js
// app/layout.js
export default function Layout({ children, analytics, team }) {
  return (
    <div>
      {children}
      <div className="grid grid-cols-2 gap-4">
        {analytics}
        {team}
      </div>
    </div>
  );
}

Intercepting Routes

app/
├── feed/
│   └── page.js
├── photo/
│   └── [id]/
│       └── page.js
└── (..)photo/
    └── [id]/
        └── page.js

Intercepting routes memungkinkan Anda menampilkan route dalam context yang berbeda (misalnya modal).

API Routes

// app/api/posts/route.js
import { NextResponse } from "next/server";

export async function GET() {
  const posts = await fetch("https://api.example.com/posts");
  const data = await posts.json();

  return NextResponse.json(data);
}

export async function POST(request) {
  const body = await request.json();

  // Process the data
  const newPost = await createPost(body);

  return NextResponse.json(newPost, { status: 201 });
}

Dynamic API Routes

// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
  const { id } = params;
  const post = await getPost(id);

  if (!post) {
    return NextResponse.json({ error: "Post not found" }, { status: 404 });
  }

  return NextResponse.json(post);
}

export async function DELETE(request, { params }) {
  const { id } = params;
  await deletePost(id);

  return NextResponse.json({ message: "Post deleted" });
}

Middleware

// middleware.js
import { NextResponse } from "next/server";

export function middleware(request) {
  // Check authentication
  const token = request.cookies.get("token");

  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // Add custom headers
  const response = NextResponse.next();
  response.headers.set("x-custom-header", "custom-value");

  return response;
}

export const config = {
  matcher: ["/dashboard/:path*", "/api/:path*"],
};

Best Practices

1. Gunakan Server Components Secara Default

Hanya gunakan Client Components ketika Anda membutuhkan interaktivitas atau browser APIs.

2. Optimasi Data Fetching

// Good: Fetch data di level yang tepat
export default async function Layout({ children }) {
  const user = await getUser(); // Fetch sekali untuk semua child routes

  return (
    <div>
      <UserProvider user={user}>{children}</UserProvider>
    </div>
  );
}

3. Gunakan Streaming untuk UX yang Lebih Baik

import { Suspense } from "react";

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading analytics...</div>}>
        <Analytics />
      </Suspense>
      <Suspense fallback={<div>Loading posts...</div>}>
        <Posts />
      </Suspense>
    </div>
  );
}

4. Error Boundaries yang Granular

Tempatkan error.js di level yang tepat untuk error handling yang lebih spesifik.

Kesimpulan

Next.js App Router memberikan cara yang lebih modern dan powerful untuk membangun aplikasi React. Dengan Server Components, improved data fetching, dan file-based routing yang fleksibel, Anda dapat membangun aplikasi yang lebih performant dan maintainable.

Mulai dengan migrasi bertahap dari Pages Router, dan manfaatkan fitur-fitur baru seperti parallel routes dan intercepting routes untuk menciptakan user experience yang lebih baik.

Mari Mulai Kolaborasi.

Ide digital Anda siap menjadi produk nyata. Konsultasikan gratis dengan tim kami dan wujudkan solusi yang tepat untuk bisnis Anda.

Kami menggunakan cookie
Kami menggunakan cookie untuk memastikan Anda mendapatkan pengalaman terbaik di website kami. Untuk informasi lebih lanjut tentang penggunaan cookie, silakan lihat kebijakan cookie kami.

Dengan mengklik "Terima", Anda menyetujui penggunaan cookie kami.

Pelajari lebih lanjut