Common Next.js Mistakes (And How to Avoid Them Like a Pro)
nextjsreactbest-practicesserver-componentsapp-router

Common Next.js Mistakes (And How to Avoid Them Like a Pro)

A comprehensive guide to avoiding the most common Next.js pitfalls. Learn how to properly use Server Components, Server Actions, caching, and more.

πŸš€ Common Next.js Mistakes (And How to Avoid Them Like a Pro)

⏱️ Reading Time: ~20 minutes


Introduction

Next.js is powerful.

Server Components, App Router, Server Actions, caching, streamingβ€”everything is designed for performance and scalability.

But…

If you use it the wrong way, you can easily destroy those benefits without even realizing it.

After building and reviewing multiple production-grade apps, I've seen the same mistakes again and again. So I decided to compile this practical checklist you can use before shipping your next feature.

Let's dive in.


1. Marking the Root Page as a Client Component

❌ Mistake

Adding "use client" to the root page forces the entire component tree to run on the client. This removes most of the performance benefits of Server Components, including server-side rendering, streaming, and caching.

This usually happens because developers assume every interactive UI needs client componentsβ€”which is not true in Next.js.

"use client";

export default function Page() {
  return <Dashboard />;
}

Here, the whole page and all child components become client-side.

βœ… Solution

Only mark the smallest interactive components as client components. Keep pages and layouts server-side whenever possible.

// page.tsx (Server Component)
export default function Page() {
  return <Dashboard />;
}
// components/Counter.tsx
"use client";

import { useState } from "react";

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

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Now, only the interactive part runs on the client.


2. Not Protecting Server Actions

❌ Mistake

Server Actions are exposed as public HTTP endpoints.

If you don't protect them, anyone can trigger themβ€”leading to data leaks, unauthorized changes, or security breaches.

Many developers assume Server Actions are private by default. They are not.

"use server";

export async function deleteUser(id: string) {
  await db.user.delete({ where: { id } });
}

This action can be called by anyone.

βœ… Solution

Always implement proper protection: authentication, validation, and authorization.

"use server";

import { auth } from "@/lib/auth";

export async function deleteUser(id: string) {
  const session = await auth();

  if (!session) {
    throw new Error("Unauthorized");
  }

  await db.user.delete({ where: { id } });
}

Now, only authenticated users can perform the action.


3. Placing Data Fetching Inside Server Action Files

❌ Mistake

Any file that contains "use server" turns all its exports into public endpoints.

If you put read-only fetch functions there, you unintentionally expose internal data.

"use server";

export async function getUsers() {
  return db.user.findMany();
}

This fetch function is now publicly accessible.

βœ… Solution

Separate read and write logic.

// lib/data.ts
export async function getUsers() {
  return db.user.findMany();
}
// actions.ts
"use server";

export async function createUser(data: any) {
  await db.user.create({ data });
}

Now, only mutations are exposed.


4. Shipping AI-Generated Code Without Review

❌ Mistake

AI tools can write code quickly, but they often miss:

  • Edge cases
  • Validation
  • Type safety
  • Security rules

Shipping AI-generated code directly to production can introduce serious bugs.

// Generated by AI
function sum(a, b) {
  return a + b;
}

No types. No validation.

βœ… Solution

Always review and improve AI-generated code.

function calculateSum(a: number, b: number): number {
  if (a < 0 || b < 0) {
    throw new Error("Invalid input");
  }

  return a + b;
}

Now the function is safe and maintainable.


5. Using API Routes for Simple GET Requests

❌ Mistake

Creating separate API routes for simple GET requests adds unnecessary complexity and often forces client-side fetching.

// app/api/posts/route.ts
export async function GET() {
  const posts = await getPosts();
  return Response.json(posts);
}
"use client";

useEffect(() => {
  fetch("/api/posts")
    .then((res) => res.json())
    .then(setPosts);
}, []);

βœ… Solution

Fetch data directly inside Server Components.

// page.tsx
export default async function Page() {
  const posts = await getPosts();

  return <PostList posts={posts} />;
}

Use Suspense instead of useState.


6. Placing a Suspense Boundary at the Wrong Level

❌ Mistake

Wrapping Suspense around an async call does not trigger loading UI correctly.

<Suspense fallback={<Loading />}>{getPosts()}</Suspense>

Suspense cannot control rendering here.

βœ… Solution

Wrap the component that consumes the data.

<Suspense fallback={<Loading />}>
  <PostList />
</Suspense>
async function PostList() {
  const posts = await getPosts();
  return <List posts={posts} />;
}

7. Mixing Up use cache and use cache private

❌ Mistake

Using shared cache for user-specific data can cause data leakage.

"use cache";

export async function getCart() {
  return getUserCart();
}

βœ… Solution

Use private cache for user data.

"use cache private";

export async function getCart() {
  return getUserCart();
}

8. Incorrectly Using Context Providers in App Router

❌ Mistake

Wrapping layout directly with a client provider makes everything client-side.

"use client";

export default function RootLayout({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}

βœ… Solution

Create a separate provider wrapper.

// Providers.tsx
"use client";

export function Providers({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}
// layout.tsx
export default function RootLayout({ children }) {
  return <Providers>{children}</Providers>;
}

9. Using Browser APIs in Server Components

❌ Mistake

Server Components cannot access browser APIs.

console.log(window.location.href);

This crashes on the server.

βœ… Solution

Move browser logic to client components.

"use client";

import { useEffect } from "react";

export function LocationLogger() {
  useEffect(() => {
    console.log(window.location.href);
  }, []);

  return null;
}

10. Adding "use client" Unnecessarily

❌ Mistake

Forms and buttons don't always need client components.

"use client";

export default function Form() {
  return <form action={saveData}></form>;
}

βœ… Solution

Let Server Actions handle it.

export default function Form() {
  return <form action={saveData}></form>;
}

11. Redirecting Inside a Try-Catch Block

❌ Mistake

redirect() works by throwing an internal error. Catching it breaks navigation.

try {
  redirect("/login");
} catch (err) {
  console.log(err);
}

βœ… Solution

Call redirect outside try-catch or rethrow.

redirect("/login");

or

catch (err) {
  throw err;
}

12. Forcing Highly Interactive UIs to Be Server Components

❌ Mistake

Interactive components on the server cause lag.

async function Counter() {
  return <button>+</button>;
}

βœ… Solution

Use client components.

"use client";

import { useState } from "react";

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

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

13. Not Using loading.tsx for Loading States

❌ Mistake

Using useState for loading forces client-side code.

const [loading, setLoading] = useState(true);

βœ… Solution

Use route-level loading UI.

// app/posts/loading.tsx
export default function Loading() {
  return <Spinner />;
}

Next.js handles it automatically.


Conclusion

Next.js is incredibly powerful when used correctly. By avoiding these common mistakes, you'll build faster, more secure, and more maintainable applications.

Remember:

βœ… Keep Server Components as the default
βœ… Protect all Server Actions
βœ… Use proper caching strategies
βœ… Leverage Next.js features like loading.tsx and Suspense
βœ… Always review AI-generated code

Want to dive deeper? Check out the official Next.js documentation for more best practices.

Happy coding! πŸš€

Related Posts

Next.js Authentication with Better Auth

Next.js Authentication with Better Auth

Learn how to implement authentication in Next.js with Better Auth, Neon, and Drizzle ORM.

nextjsbetter-authauthentication+1 more
Read More

β€œSelf-control leads one to the highest perfection.”

β€” Shree Krishna, Bhagavad Gita