Common Next.js Mistakes
Dipesh Aryal/June 16, 2025
A deep dive into real-world mistakes I made when learning Next.js — and how to fix them.
These mistakes slowed me down when learning Next.js. Here's how to avoid them and build cleaner, faster apps.
⚛️ Rendering Mistakes
1. Using "use client"
too high in the tree
This makes all child components client components — even those that don't need it, which ruins the benefit of server components.
// 🚫 Bad: All children become client components
'use client'
export default function Layout({ children }) {
return <div>{children}</div>
}
✅ Fix: Only use "use client"
in components that actually need it (e.g., interactive components using state, effects).
// ✅ Good: Only the component that needs interactivity is client
'use client'
export default function Toggle() {
const [on, setOn] = useState(false)
return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>
}
2. Not refactoring client components
A common habit is dumping "use client"
on top-level files. Instead:
- Extract the logic into small client components
- Compose them in server components using props or
children
// ServerComponent.tsx
export default function Wrapper({ children }) {
return <div className="p-4">{children}</div>
}
// ClientComponent.tsx
;('use client')
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
3. Import location matters
You can use client components inside server components. Just don’t import them directly. Instead:
✅ Pass them as children
or load with dynamic imports.
4. Using client-side state libraries in server components
Libraries like Redux
, Zustand
, or Context
are client-only.
✅ Use Next.js tools like searchParams
, cookies
, or directly mutate DB state from the server.
5. Misunderstanding "use server"
This does not make a component a server component. It marks a function as a Server Action.
'use server'
export async function saveForm(data) {
// DB logic
}
6. Leaking sensitive data from RSC to RCC
Passing secrets like tokens or credentials as props exposes them to the client.
✅ Keep sensitive logic and data strictly in the server.
7. Using localStorage
directly in components
Even client components run once on the server. So:
// 🚫 This breaks:
const theme = localStorage.getItem('theme')
✅ Use useEffect()
:
useEffect(() => {
const theme = localStorage.getItem('theme')
}, [])
8. Hydration mismatch
Server HTML and client HTML must match.
✅ Prevent by delaying state-based rendering:
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
✅ Or use:
<Component suppressHydrationWarning />
9. 3rd party libraries using browser APIs
Libraries that use document
, window
, etc., will break in SSR.
✅ Use dynamic imports with ssr: false
:
const RichEditor = dynamic(() => import('./RichEditor'), { ssr: false })
🖥️ Server Component Mistakes
10. Still using route handlers for everything
You don't need /api/route
for simple form submissions.
✅ Use Server Actions where possible.
11. Lifting state unnecessarily
Next.js caches fetches per request. Fetch in multiple places without prop drilling.
12. Waterfall fetching
Avoid sequential fetching of unrelated data.
// ✅ Use Promise.all
const [posts, users] = await Promise.all([getPosts(), getUsers()])
13. Nested waterfall
Avoid await
in deep components. Fetch higher and pass down as props.
14. Data not updating after server action
✅ Use:
revalidatePath('/dashboard')
15. Submitting forms to RSCs
✅ Use server actions with <form action={addTodo}>
.
16. Using server actions in RCC
✅ Possible, but type arguments as unknown
and validate manually.
17. Not validating data
Always validate on the server too.
✅ Use Zod, Yup, or custom checks.
18. Using "use server"
incorrectly
✅ If you're trying to force server-side code, use server-only
package instead.
19. Confusing params
and searchParams
params
: route segment like/user/[id]
searchParams
: query string like?tab=profile
20. Not using searchParams for state
URL-based state (filters, tabs) is shareable and cached.
21. No loading/error UI
✅ Use loading.tsx
, error.tsx
, and <Suspense fallback={...}>
.
22–24. Suspense misuse
- Be granular.
- Use
key
for dynamic fallback rendering.
<Suspense key={slug} fallback={<Skeleton />}>
<Post slug={slug} />
</Suspense>
25. Accidentally forcing dynamic rendering
Using cookies, headers()
, or dynamic imports in shared layouts like navbars or footers can make entire app dynamic.
✅ Use cache: 'force-cache'
where possible.
26. Hardcoding secrets
✅ Use environment variables and server-only
package to protect secrets.
import 'server-only'
const token = process.env.PRIVATE_KEY
27. Mixing server/client logic
✅ Keep folders like:
/utils
/client/useLocalStorage.ts
/server/db.ts
28. Using redirect()
inside try-catch
✅ Call redirect()
only after your logic. Don't wrap it inside try/catch
, since it throws internally.
await doSomething()
redirect('/success') // not in try
🧠 Final Thoughts
Mistakes are part of learning. Documenting them helps you (and others) grow.