15 Kesalahan Web Developer Ketika Pakai Next JS Beserta Contoh Kodingan

NextJS kini telah menjadi framework andalan bagi banyak developer yang ingin membangun aplikasi React dengan performa tinggi dan pengalaman pengguna yang mulus. Kepopuleran NextJS didukung oleh fitur-fitur modern seperti server-side rendering, static site generation, hingga kemudahan integrasi API, yang menjadikannya solusi serbaguna untuk berbagai kebutuhan pengembangan web masa kini.

Namun, tahukah kamu bahwa bahkan developer berpengalaman pun masih sering jatuh ke dalam “jebakan” yang sama saat menggunakan NextJS? Kesalahan-kesalahan kecil ini bisa berdampak besar, mulai dari menurunkan performa aplikasi, menimbulkan bug yang sulit dilacak, hingga membuat aplikasi jadi sulit di maintain dalam jangka panjang.

Kalau kamu ingin membuat aplikasi NextJS yang benar-benar scalable, cepat, dan bebas masalah, penting untuk mengenali kesalahan-kesalahan umum sejak awal.

Yuk, simak pembahasannya dan tingkatkan kemampuan NextJS kamu ke level berikutnya!

1. Salah Memahami Client vs Server Components

Ilustrasi client vs server components
Ilustrasi client vs server components

Kesalahan ini sangat umum terjadi sejak NextJS 13 memperkenalkan App Router. Banyak developer mencoba menggunakan hooks React seperti useState atau useEffect di Server Components tanpa menandainya sebagai Client Component.

Contoh Kesalahan:

// app/page.js
export default function HomePage() {
  const [count, setCount] = useState(0); // ❌ Error!
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Pada contoh kesalahan di atas, useState membuat state yang hidup pada lifecycle komponen di sisi klien. Di server tidak ada "lifecycle klien" yang mempertahankan state antar interaksi pengguna, karena server hanya menghasilkan HTML statis saat render. Oleh karena itu pemanggilan useState di server component akan memicu error build atau runtime saat NextJS mencoba mengompilasi komponen tersebut. Biasanya NextJS memberi tahu bahwa hook tidak boleh dipanggil di server component atau bahwa komponen harus diberi directive use client jika ingin menggunakan fitur klien.

Solusi:

// app/page.js
'use client'; // ✅ Tambahkan directive ini

import { useState } from 'react';

export default function HomePage() {
  const [count, setCount] = useState(0);
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Solusi di atas, dengan menambahkan 'use client' di awal file, NextJS memastikan seluruh komponen dijalankan di browser sebagai client component. Ini artinya modul bisa memakai hook seperti useState, menangani event DOM, dan melakukan re-render ketika state berubah, sehingga fitur interaktif dapat berjalan di sisi klien.

2. Tidak Mengoptimalkan Image Component

Ilustrasi image component
Ilustrasi image component

NextJS menyediakan komponen Image yang sangat powerful untuk melakukan optimasi gambar secara otomatis, seperti kompresi, responsif, lazy loading, dan penggunaan format gambar paling efisien sesuai device. Namun, banyak developer masih menggunakan tag <img> standar yang tidak memiliki fitur optimasi bawaan.

Contoh Kesalahan:

// ❌ Tidak optimal
<img src="/hero.jpg" alt="Hero" />

Pada contoh kesalahan di atas, gambar hanya dimunculkan memakai tag <img>. Tag ini memang akan menampilkan gambar secara langsung, tetapi browser tidak melakukan optimasi otomatis seperti kompresi, lazy loading (menunda load gambar yang belum tampil di layar), atau penyesuaian ukuran gambar untuk layar yang berbeda. Akibatnya, gambar bisa memperlambat waktu loading situs, terutama pada koneksi internet lambat atau device dengan layar kecil.

Solusi:

// ✅ Gunakan NextJS Image
import Image from 'next/image';

<Image 
  src="/hero.jpg" 
  alt="Hero" 
  width={800} 
  height={600}
  priority
/>

Pada solusi di atas, menggunakan komponen Image dari NextJS, yang jauh lebih unggul dibandingkan tag <img> biasa. Dengan mengimpor langsung dari 'next/image', gambar yang dimuat akan mendapatkan beragam optimasi secara otomatis. Saat menetapkan atribut src dan alt, kita tetap memastikan gambar tampil sesuai sumber yang diinginkan dan tetap memperhatikan aksesibilitas serta SEO.

3. Fetching Data di Client Padahal Bisa di Server

Ilustrasi fetching data
Ilustrasi fetching data

Salah satu kekuatan NextJS adalah dengan menggunakan server-side rendering (SSR). Dengan server-side rendering, proses pengambilan data dan pembuatan halaman dapat dilakukan lebih dulu di server sebelum dikirim ke browser pengguna. Namun, banyak developer masih melakukan data fetching di client menggunakan useEffect, padahal bisa dilakukan di server untuk performa lebih baik.

Contoh Kesalahan:

'use client';
import { useState, useEffect } from 'react';

export default function ProductPage() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    // ❌ Fetching di client
    fetch('/api/products')
      .then(res => res.json())
      .then(data => setProducts(data));
  }, []);
  
  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}

Pada contoh kesalahan di atas, data produk diambil langsung di sisi client menggunakan hook useEffect. Setiap kali halaman dimuat, browser akan menjalankan kode di dalam useEffect untuk mengambil data produk dari endpoint /api/products, kemudian menyimpannya di state lokal menggunakan useState. Cara ini memang bisa berjalan, namun tidak optimal karena membuat pengguna harus menunggu proses fetch selesai di sisi browser sebelum data produk muncul di layar. Selain itu, pendekatan ini juga kurang baik untuk SEO dan dapat memperlambat waktu tampil laman, khususnya jika data yang diambil cukup besar atau penting bagi tampilan utama halaman.

Solusi:

// ✅ Fetching di server (App Router)
async function getProducts() {
  const res = await fetch('<https://api.example.com/products>');
  return res.json();
}

export default async function ProductPage() {
  const products = await getProducts();
  
  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}

Solusi di atas, proses fetching data dilakukan langsung di server sebelum komponen dirender. Fungsi getProducts melakukan fetch ke API eksternal, lalu data produk diterima hasilnya lewat await. Karena pemanggilan API terjadi sebelum halaman dikirimkan ke browser, maka data produk sudah siap ketika user membuka halaman, sehingga loading lebih cepat dan SEO juga lebih baik. Selain itu, pengguna tidak akan melihat halaman kosong atau loading spinner ketika data sedang diambil, karena semuanya sudah diproses di server sebelum sampai ke client. Pendekatan ini jauh lebih efisien dan memberikan pengalaman yang lebih optimal kepada pengguna.

4. Tidak Menggunakan Dynamic Import untuk Code Splitting

Ilustrasi dynamic import - generated by Gemini AI
Ilustrasi dynamic import - generated by Gemini AI

Memuat semua komponen sekaligus akan membuat bundle size membengkak. Dynamic import membantu memecah kode menjadi chunk-chunk kecil yang dimuat sesuai kebutuhan.

Contoh Kesalahan:

// ❌ Import statis untuk komponen berat
import HeavyChart from '@/components/HeavyChart';

export default function Dashboard() {
  return (
    <div>
      <HeavyChart data={data} />
    </div>
  );
}

Pada contoh kesalahan di atas, komponen HeavyChart diimpor secara statis ke dalam file. Artinya, setiap kali halaman dashboard dimuat, seluruh kode untuk komponen HeavyChart ikut dimasukkan ke dalam bundle utama aplikasi, tidak peduli apakah pengguna benar-benar membutuhkannya di awal atau tidak. Jika komponen tersebut ukurannya besar atau hanya digunakan pada kondisi tertentu, cara ini akan meningkatkan ukuran file JavaScript yang harus diunduh dan dieksekusi oleh browser. Akibatnya, waktu loading halaman bisa menjadi lebih lama dan pengalaman pengguna pun berkurang, terutama untuk aplikasi berskala besar dengan banyak fitur.

Solusi:

// ✅ Dynamic import
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false // jika tidak perlu SSR
});

export default function Dashboard() {
  return (
    <div>
      <HeavyChart data={data} />
    </div>
  );
}

Solusi di atas menggunakan dynamic import dengan bantuan fungsi dynamic dari NextJS. Dengan pendekatan ini, komponen HeavyChart tidak langsung dimasukkan ke bundle utama. Sebagai gantinya, kode untuk komponen tersebut dipisahkan menjadi chunk atau potongan terpisah yang hanya akan dimuat saat dibutuhkan, biasanya ketika bagian dashboard di render dan komponen tersebut benar-benar digunakan. Selain itu, dynamic import juga memungkinkan penambahan tampilan loading sederhana seperti paragraf “Loading chart…” selama proses pemuatan komponen berlangsung.

5. Lupa Menggunakan Metadata API

Ilustrasi metadata api - generated by Gemini AI
Ilustrasi metadata api - generated by Gemini AI

SEO adalah aspek penting dari web development. Next.js 13+ menyediakan Metadata API yang mudah digunakan, namun sering diabaikan.

Contoh Kesalahan:

// ❌ Tidak ada metadata
export default function AboutPage() {
  return <div>About Us</div>;
}

Pada contoh kesalahan di atas, halaman AboutPage hanya menampilkan konten tanpa menyisipkan metadata apa pun. Padahal, metadata seperti judul halaman (title), deskripsi, dan informasi Open Graph sangat penting agar website lebih mudah ditemukan oleh mesin pencari seperti Google dan tampak menarik ketika dibagikan di media sosial. Tanpa metadata, halaman Anda akan sulit bersaing dalam peringkat pencarian dan tampil kurang informatif ketika di preview di platform lain.

Solusi:

// ✅ Gunakan Metadata API
export const metadata = {
  title: 'About Us - Company Name',
  description: 'Learn more about our company and mission',
  openGraph: {
    title: 'About Us',
    description: 'Learn more about our company',
    images: ['/og-image.jpg']
  }
};

export default function AboutPage() {
  return <div>About Us</div>;
}

Solusi optimal adalah memakai Metadata API bawaan Next.js dengan mendefinisikan ekspor metadata sebelum fungsi halaman. Hal ini memudahkan pengaturan judul, deskripsi, dan openGraph, sehingga metadata otomatis tertanam di HTML halaman. Metode ini secara langsung meningkatkan SEO dan memastikan tampilan pratinjau yang menarik saat halaman dibagikan ke media sosial.

6. Tidak Memahami Caching Strategy

Ilustrasi caching strategy - generated by Gemini AI
Ilustrasi caching strategy - generated by Gemini AI

NextJS memiliki sistem caching yang sophisticated, namun tidak memahaminya bisa menyebabkan data stale atau performa buruk.

Contoh Kesalahan:

// ❌ Fetch tanpa revalidation strategy
async function getData() {
  const res = await fetch('<https://api.example.com/posts>');
  return res.json();
}

Pada contoh kesalahan di atas, data diambil dari API eksternal menggunakan fungsi fetch tanpa menentukan strategi caching apa pun. Secara default, jika fetch digunakan tanpa pengaturan tambahan dalam NextJS, data yang diambil bisa saja menjadi usang (stale) atau tidak selalu paling terbaru, tergantung bagaimana server dan NextJS mengelola cache secara otomatis. Hal ini bisa menyebabkan pengalaman pengguna terganggu karena data yang mereka lihat tidak selalu akurat, atau di sisi lain performa aplikasi menjadi kurang optimal akibat pengambilan data yang terlalu sering.

Solusi:

// ✅ Tentukan strategi caching yang tepat
async function getData() {
  const res = await fetch('<https://api.example.com/posts>', {
    next: { revalidate: 3600 } // revalidate setiap 1 jam
  });
  return res.json();
}

// Atau untuk data yang sangat dinamis
async function getRealTimeData() {
  const res = await fetch('<https://api.example.com/live>', {
    cache: 'no-store' // selalu fetch fresh data
  });
  return res.json();
}

Solusi terbaik adalah menyesuaikan strategi caching dengan karakter data. Untuk data yang jarang berubah, gunakan next: { revalidate: 3600 } agar data di-refresh setiap 1 jam (irit resource dan tetap up-to-date). Jika datanya bersifat real-time, pakai cache: 'no-store' supaya fetch selalu mengambil data terbaru langsung dari API dan tidak pernah memakai cache.

7. Menggunakan getServerSideProps untuk Data Statis

Ilustrasi getServerSideProps NextJS - generated by Gemini AI
Ilustrasi getServerSideProps NextJS - generated by Gemini AI

Pada Pages Router, banyak developer menggunakan getServerSideProps untuk semua kebutuhan data fetching, padahal getStaticProps lebih efisien untuk data yang jarang berubah.

Contoh Kesalahan:

// ❌ SSR untuk konten yang jarang berubah
export async function getServerSideProps() {
  const res = await fetch('<https://api.example.com/about>');
  const data = await res.json();
  
  return { props: { data } };
}

Pada contoh kesalahan di atas, pengambilan data untuk halaman "About" dilakukan menggunakan getServerSideProps. Fungsi ini akan men-fetch data setiap kali ada permintaan halaman, atau setiap kali pengguna mengakses laman tersebut. Sebenarnya, ini cara yang kurang efisien jika data yang ditampilkan jarang berubah, seperti informasi profil perusahaan atau visi-misi yang hanya diupdate sesekali. Akibatnya, beban server menjadi lebih tinggi, loading halaman cenderung lebih lama, dan performa aplikasi secara keseluruhan bisa menurun padahal respons yang diminta hampir selalu sama.

Solusi:

// ✅ Gunakan Static Generation dengan revalidation
export async function getStaticProps() {
  const res = await fetch('<https://api.example.com/about>');
  const data = await res.json();
  
  return { 
    props: { data },
    revalidate: 86400 // revalidate setiap 24 jam
  };
}

Solusi di atas memanfaatkan getStaticProps untuk kebutuhan data yang statis atau jarang diperbarui. Dengan static generation, NextJS akan mengambil data satu kali saja pada saat proses build proyek, lalu menyimpan hasil render menjadi file HTML statis. Pengunjung akan merasakan waktu muat yang sangat cepat, dan beban server berkurang drastis. Dalam contoh solusi, ditambahkan opsi revalidate: 86400 sehingga halaman akan otomatis diupdate (diregenerate) setiap 24 jam. Dengan cara ini, konten tetap segar tanpa perlu melakukan build manual secara rutin, sementara performa dan efisiensinya tetap optimal.

8. Tidak Memanfaatkan Parallel Routes dan Loading States

Ilustrasi loading states
Ilustrasi loading states

NextJS App Router menyediakan fitur loading states otomatis melalui file loading.js, namun banyak yang tidak memanfaatkannya.

Contoh Kesalahan:

// app/dashboard/page.js
// ❌ Tidak ada loading state
export default async function Dashboard() {
  const data = await fetchDashboardData(); // loading lama tanpa feedback
  return <div>{data.content}</div>;
}

Pada contoh kesalahan di atas, halaman dashboard mengambil data dengan proses asynchronous langsung di fungsi komponen tanpa memberikan indikasi apa-apa kepada pengguna selama data sedang di-fetch. Akibatnya, jika proses pengambilan data berlangsung lama atau respons lambat, pengguna hanya melihat halaman kosong tanpa feedback, sehingga pengalaman pengguna menjadi kurang nyaman dan bisa membuat mereka mengira aplikasi sedang bermasalah atau error.

Solusi:

// app/dashboard/loading.js
// ✅ Buat file loading.js
export default function Loading() {
  return <div>Loading dashboard...</div>;
}

// app/dashboard/page.js
export default async function Dashboard() {
  const data = await fetchDashboardData();
  return <div>{data.content}</div>;
}

NextJS App Router menyediakan fitur loading state otomatis dengan membuat file khusus bernama loading.js di dalam folder route. Pada contoh solusi, file loading.js diletakkan pada direktori yang sama dengan halaman dashboard, dan berisi komponen sederhana yang menampilkan pesan “Loading dashboard...”. Ketika halaman Dashboard melakukan fetching data, NextJS secara otomatis akan menampilkan loading state ini, sehingga pengguna diberikan informasi bahwa proses pemuatan dashboard sedang berlangsung. Cara ini tidak hanya meningkatkan pengalaman pengguna dengan feedback visual yang jelas, tetapi juga membuat aplikasi terasa lebih interaktif dan profesional.

9. Salah Menggunakan Environment Variables

Ilustrasi environment variables - generated by Gemini AI
Ilustrasi environment variables - generated by Gemini AI

Environment variables di NextJS memiliki aturan khusus, terutama untuk variabel yang perlu diakses di browser.

Contoh Kesalahan:

// ❌ Tidak bisa diakses di browser
// .env.local
API_KEY=secret123

// Component
export default function Page() {
  const apiKey = process.env.API_KEY; // undefined di browser!
  return <div>{apiKey}</div>;
}

Pada contoh kesalahan di atas, environment variable API_KEY didefinisikan di file .env.local tanpa prefix khusus dan langsung dicoba diakses dari komponen frontend menggunakan process.env.API_KEY. Namun, variabel ini akan selalu bernilai undefined jika diakses di browser, karena NextJS hanya memasukkan environment variables ke bundle frontend jika namanya diawali dengan NEXT_PUBLIC_. Ini merupakan mekanisme keamanan yang diterapkan NextJS agar variabel yang sifatnya sensitif atau rahasia tidak secara tidak sengaja terbuka ke publik di sisi client.

Solusi:

# ✅ Gunakan prefix NEXT_PUBLIC_ untuk browser
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
API_SECRET=secret123 # hanya untuk server

Solusi di atas adalah dengan membedakan variabel yang memang perlu diakses di browser dengan memberi prefix NEXT_PUBLIC_. Sebagai contoh, jika memiliki endpoint API publik, kamu bisa mendefinisikan NEXT_PUBLIC_API_URL di .env.local, sehingga variabel ini dapat diakses pada komponen frontend menggunakan process.env.NEXT_PUBLIC_API_URL. Sedangkan variabel rahasia seperti API_SECRET tetap tanpa prefix dan hanya akan tersedia ketika kode dijalankan di server (misalnya pada API Routes atau fungsi server-side).

10. Tidak Menggunakan Route Handlers dengan Benar

Ilustrasi route handlers NextJS
Ilustrasi route handlers NextJS

Route Handlers (App Router) atau API Routes (Pages Router) sering tidak dioptimalkan dengan baik, terutama dalam hal error handling dan response format.

Contoh Kesalahan:

// app/api/users/route.js
// ❌ Tanpa error handling yang proper
export async function GET() {
  const users = await db.users.findMany();
  return Response.json(users);
}

Pada contoh kesalahan di atas, route handler untuk endpoint GET hanya melakukan pengambilan data langsung dari database lalu mengirimkan respons tanpa menangani kemungkinan error sama sekali. Kondisi ini berisiko karena jika terjadi kegagalan saat mengambil data dari database, misalnya koneksi putus atau query gagal pengguna atau client API tidak akan mendapatkan informasi yang jelas tentang error yang terjadi, bahkan bisa saja menerima response kosong atau format yang tidak terstruktur.

Solusi:

// app/api/users/route.js
// ✅ Dengan error handling dan status code yang tepat
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    const users = await db.users.findMany();
    
    return NextResponse.json(
      { success: true, data: users },
      { status: 200 }
    );
  } catch (error) {
    return NextResponse.json(
      { success: false, error: 'Failed to fetch users' },
      { status: 500 }
    );
  }
}

Solusi terbaik adalah membungkus logic pengambilan data di route handler dengan blok try-catch dan mengirim response JSON dengan format konsisten. Jika sukses, kirim status 200 dan success: true beserta data. Jika gagal, kirim status 500 dan success: false dengan pesan error. Cara ini membuat client mudah mengenali hasil API dan memastikan response selalu mudah diproses oleh frontend.

11. Mengabaikan Font Optimization

Ilustrasi font optimization NextJS
Ilustrasi font optimization NextJS

NextJS menyediakan next/font untuk optimasi font otomatis, namun masih banyak yang menggunakan Google Fonts secara manual.

Contoh Kesalahan:

// ❌ Import font manual dari CDN
import Head from 'next/head';

export default function Layout({ children }) {
  return (
    <>
      <Head>
        <link href="<https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap>" rel="stylesheet" />
      </Head>
      {children}
    </>
  );
}

Pada contoh kesalahan di atas, font dari Google Fonts diimpor secara manual melalui tag <link> di dalam elemen <Head>. Cara ini memang dapat menampilkan font Inter pada aplikasi Next.js, tapi membuat proses pemuatan font kurang optimal. Mengimpor font manual melalui CDN memungkinkan terjadinya “render-blocking” yaitu browser harus menunggu font selesai dimuat sebelum menampilkan teks dengan gaya yang benar. Selain itu, metode ini juga kurang mendukung caching yang efisien dan optimalisasi bundling, sehingga waktu muat pertama halaman bisa menjadi lebih lama.

Solusi:

// ✅ Gunakan next/font
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function Layout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Solusi modern untuk mengatur font di Nextjs adalah dengan menggunakan utility next/font. Misalnya, font Inter diimpor dari 'next/font/google', dikonfigurasi sesuai kebutuhan dan class-nya langsung diterapkan ke elemen <html>. Dengan fitur ini, pemuatan dan optimasi font dikelola otomatis, sehingga tampilan teks menjadi lebih cepat dan konsisten tanpa blocking, sekaligus menjaga performa aplikasi di semua perangkat.

12. Tidak Memanfaatkan Middleware untuk Logic Global

Ilustrasi middleware NextJS - generated by Gemini AI
Ilustrasi middleware NextJS - generated by Gemini AI

Middleware sangat powerful untuk autentikasi, redirects, atau logic global lainnya, namun sering diabaikan.

Contoh Kesalahan:

// ❌ Cek auth di setiap halaman
export default function ProtectedPage() {
  const { user } = useAuth();
  
  if (!user) {
    redirect('/login');
  }
  
  return <div>Protected Content</div>;
}

Pada contoh kesalahan di atas, pengecekan status autentikasi dilakukan langsung di dalam komponen setiap halaman. Metode ini mengharuskan Anda menulis kode autentikasi berulang-ulang di setiap komponen yang ingin dilindungi, seperti memanggil hook useAuth() dan melakukan redirect ke halaman login bila user belum terautentikasi. Akibatnya, selain membuat kode jadi tidak efisien dan sulit di maintain, proses validasi akses baru terjadi setelah halaman dirender di client sehingga lebih mudah di bypass dan tidak ideal untuk keamanan.

Solusi:

// middleware.js
// ✅ Centralized authentication check
import { NextResponse } from 'next/server';

export function middleware(request) {
  const token = request.cookies.get('token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: '/dashboard/:path*'
};

Solusi terbaik adalah menggunakan fitur middleware NextJS untuk memeriksa autentikasi secara global sebelum halaman dirender. Middleware di file middleware.js bisa mengecek token cookies saat akses ke route seperti /dashboard, lalu otomatis redirect ke login jika token tidak ada. Dengan cara ini, autentikasi terpusat dan efisien, sehingga aplikasi lebih aman dan pengecekan tidak perlu ditulis di setiap halaman.

13. Overusing Client Components

Ilustrasi overusing client components - generated by Gemini AI
Ilustrasi overusing client components - generated by Gemini AI

Sejak App Router menggunakan Server Components sebagai default, banyak developer yang terlalu cepat menambahkan 'use client' tanpa mempertimbangkan apakah benar-benar diperlukan.

Contoh Kesalahan:

// ❌ Seluruh component jadi client component
'use client';

import { useState } from 'react';

export default function Page() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <StaticContent /> {/* Tidak perlu jadi client component */}
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
}

Pada contoh kesalahan di atas, seluruh komponen halaman ditandai sebagai client component dengan menambahkan deklarasi 'use client' di bagian atas file. Cara ini menyebabkan suasana rendering berjalan di sisi browser, yang artinya seluruh kode dan dependensi harus terkirim ke client, termasuk bagian-bagian statis yang sebenarnya tidak membutuhkan interaktivitas sama sekali. Misalnya, komponen StaticContent pada contoh tidak memerlukan state atau event handler, sehingga tidak perlu dijadikan client component. Praktik ini mengakibatkan ukuran bundle aplikasi menjadi lebih besar, performa lebih rendah, dan pemrosesan di server menjadi tidak optimal.

Solusi:

// ✅ Pisahkan interactive component
// components/Counter.jsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// app/page.jsx (Server Component)
import Counter from '@/components/Counter';

export default function Page() {
  return (
    <div>
      <StaticContent /> {/* Server Component */}
      <Counter /> {/* Client Component */}
    </div>
  );
}

Solusi paling efisien adalah memisahkan komponen interaktif (seperti Counter) ke dalam client component, sementara bagian lainnya tetap sebagai server component. Dengan begitu, hanya komponen yang butuh interaksi yang dikirim ke browser, sedangkan konten utama dirender di server. Strategi ini membuat aplikasi lebih ringan, cepat, dan efisien.

14. Tidak Menggunakan Suspense Boundaries

Ilustrasi suspense boundaries - generated by Gemini AI
Ilustrasi suspense boundaries - generated by Gemini AI

Suspense memungkinkan streaming HTML dan memberikan loading state yang granular, namun sering tidak dimanfaatkan.

Contoh Kesalahan:

// ❌ Semua data harus selesai sebelum render
export default async function Page() {
  const [posts, comments, users] = await Promise.all([
    fetchPosts(),
    fetchComments(),
    fetchUsers()
  ]);
  
  return (
    <div>
      <Posts data={posts} />
      <Comments data={comments} />
      <Users data={users} />
    </div>
  );
}

Pada contoh kesalahan di atas, halaman akan menunggu seluruh data dari posts, comments, hingga users selesai di fetch sebelum merender apa pun ke pengguna. Ini berarti jika ada satu data yang lambat diambil, seluruh halaman akan ikut tertunda dan browser tidak menampilkan konten sama sekali sampai semua proses selesai. Akibatnya, pengalaman pengguna menjadi kurang baik karena waktu tunggu yang lebih lama dan tidak ada feedback visual selama proses loading berlangsung.

Solusi:

// ✅ Gunakan Suspense untuk streaming
import { Suspense } from 'react';

async function Posts() {
  const posts = await fetchPosts();
  return <div>{/* render posts */}</div>;
}

async function Comments() {
  const comments = await fetchComments();
  return <div>{/* render comments */}</div>;
}

export default function Page() {
  return (
    <div>
      <Suspense fallback={<div>Loading posts...</div>}>
        <Posts />
      </Suspense>
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments />
      </Suspense>
    </div>
  );
}

Solusi yang benar adalah dengan memanfaatkan Suspense boundaries, setiap bagian halaman bisa menampilkan loading state secara terpisah dan konten muncul bertahap sesuai datanya. Komponen seperti Posts dan Comments dibungkus dalam <Suspense> dengan fallback masing-masing, sehingga pengguna langsung melihat data yang sudah siap tanpa menunggu semua fetch selesai. Hal ini meningkatkan performa halaman dan membuat user experience lebih responsif serta informatif.

15. Tidak Memahami Hydration Errors

Ilustrasi hydration errors - generated by Gemini AI

Ilustrasi hydration errors - generated by Gemini AI

Hydration error terjadi ketika HTML yang di render di server tidak cocok dengan yang di render di client. Ini sering terjadi dengan konten dinamis seperti timestamp atau random values.

Contoh Kesalahan:

// ❌ Menyebabkan hydration mismatch
export default function Page() {
  return (
    <div>
      <p>Current time: {new Date().toLocaleString()}</p>
    </div>
  );
}

Pada contoh kesalahan di atas, Jika kita menampilkan waktu menggunakan new Date().toLocaleString() langsung di body komponen maka waktu akan dirender di server dan bisa berbeda saat klien melakukan hydration. Hal ini karena NextJS merender HTML di server, lalu React mencoba mencocokkan hasil render di browser. Jika nilai waktu berubah, terjadi mismatch yang memicu hydration warning atau error di konsol browser.

Solusi:

// ✅ Gunakan client component untuk konten dinamis
'use client';

import { useState, useEffect } from 'react';

export default function Page() {
  const [time, setTime] = useState('');
  
  useEffect(() => {
    setTime(new Date().toLocaleString());
  }, []);
  
  return (
    <div>
      <p>Current time: {time || 'Loading...'}</p>
    </div>
  );
}

// Atau gunakan suppressHydrationWarning untuk kasus tertentu
export default function Page() {
  return (
    <div>
      <p suppressHydrationWarning>
        Current time: {new Date().toLocaleString()}
      </p>
    </div>
  );
}

Solusi terbaik untuk konten dinamis seperti waktu atau data random adalah dengan memindahkan logikanya ke client component. Tambahkan deklarasi ‘use client’, simpan nilai pada state dan update menggunakan useEffect agar data diproses di sisi client, sehingga server tidak perlu merender data yang selalu berubah dan tidak terjadi mismatch. Jika terpaksa harus render di server, gunakan properti suppressHydrationWarning pada elemen terkait agar React mengabaikan mismatch antara server dan client, tapi cara ini hanya cocok untuk kasus khusus dan bukan solusi ideal jangka panjang.

Penutup

NextJS adalah framework yang powerful, namun memiliki learning curve tersendiri. Kesalahan-kesalahan di atas adalah hal yang sangat umum terjadi, bahkan pada developer berpengalaman. Yang penting adalah terus belajar dan memahami konsep-konsep fundamental seperti Server vs Client Components, caching strategy, dan optimisasi performa. Dengan menghindari 15 kesalahan ini, aplikasi NextJS kamu akan lebih performan, maintainable, dan memberikan pengalaman pengguna yang lebih baik. Jika kamu ingin memperdalam skill dan membangun project nyata dengan NextJS, jangan lupa untuk mengikuti kelas di BuildWithAngga. Di sana, kamu bisa belajar langsung dari praktisi berpengalaman dan mempersiapkan portfolio profesional sebagai developer.

Referensi

  1. Docs Next.js

by Dani Aprilyanto