DOMAIN:FRONTEND:PITFALLS¶
OWNER: floris (Team Alpha), floor (Team Beta)
UPDATED: 2026-03-24
READ_BEFORE: every frontend task
SEVERITY_CONTEXT: these are real production bugs and performance killers. Not theoretical.
PITFALL:USE_CLIENT_EVERYWHERE¶
SYMPTOM: entire pages marked "use client". Bundle size 3x larger than necessary. No Server Component benefits.
FREQUENCY: extremely common with LLM-generated code.
ANTI_PATTERN: adding "use client" at the top of a page because one child needs useState.
ANTI_PATTERN: adding "use client" because you saw an import warning about a client-only library.
ANTI_PATTERN: LLM generates "use client" by default because it was trained on pre-App-Router patterns.
// BAD: entire page is client because of one toggle
"use client";
import { useState } from "react";
import { db } from "@/lib/db"; // ERROR: db cannot be used in client component
export default function ProductsPage() {
const [showFilters, setShowFilters] = useState(false);
// ... entire page is now client-side
}
// GOOD: isolate the interactive part
// app/products/page.tsx (Server Component — no directive)
import { db } from "@/lib/db";
import { FilterToggle } from "@/components/filter-toggle"; // "use client"
export default async function ProductsPage() {
const products = await db.query.products.findMany();
return (
<div>
<FilterToggle /> {/* Only this tiny component is client */}
<ProductGrid products={products} />
</div>
);
}
FIX: push "use client" boundary to the smallest leaf component.
CHECK: run next build and review per-route JS sizes. If a page has >100KB client JS, investigate.
PITFALL:PROP_DRILLING_INSTEAD_OF_COMPOSITION¶
SYMPTOM: props passed through 5+ levels of components. Components in the middle have props they don't use.
FREQUENCY: very common with LLM-generated code.
ANTI_PATTERN: passing user, theme, onAction through Layout → Page → Section → Card → Button.
// BAD: drilling user through components that don't use it
function Layout({ user }) {
return <Sidebar user={user}><Content user={user} /></Sidebar>;
}
function Content({ user }) {
return <Card user={user} />;
}
function Card({ user }) {
return <Avatar user={user} />;
}
FIX_1: use composition — pass the rendered component, not data.
// GOOD: composition avoids drilling
function Layout({ children }) {
return <Sidebar>{children}</Sidebar>;
}
// Usage: <Layout><Card><Avatar user={user} /></Card></Layout>
FIX_2: use Context for truly global data (auth user, theme).
FIX_3: fetch data where it is needed (Server Components can fetch independently).
PITFALL:HYDRATION_MISMATCHES¶
SYMPTOM: "Text content does not match server-rendered HTML", UI flicker, broken interactivity.
FREQUENCY: common. See ssr-hydration.md for full details.
TOP_CAUSES:
1. new Date() or Date.now() in server-rendered component → different on server vs client
2. Math.random() for keys or IDs → different on server vs client
3. window, document, localStorage access during render → undefined on server
4. <div> inside <p> or <a> inside <a> → browser auto-corrects HTML
5. Theme class on <html> without suppressHydrationWarning
6. Browser extension injecting attributes (dev-only, ignore)
QUICK_FIX_CHECKLIST:
- [ ] replace Math.random() IDs with useId()
- [ ] move Date formatting to useEffect or pass from server
- [ ] move browser API access to useEffect
- [ ] validate HTML nesting (no block elements inside p/span)
- [ ] add suppressHydrationWarning on html tag for theme providers
PITFALL:BARREL_FILE_IMPORTS¶
SYMPTOM: massive bundle sizes. "Server Component" errors in client barrel. Build failures.
FREQUENCY: common in monorepos and larger projects.
ANTI_PATTERN: re-exporting everything from components/index.ts.
// components/index.ts (BARREL FILE — DO NOT CREATE)
export { Button } from "./ui/button";
export { Dialog } from "./ui/dialog";
export { ServerOnlyComponent } from "./server-only"; // BREAKS: server component in client barrel
export { HeavyChart } from "./charts/heavy-chart"; // BUNDLES: even if unused
PROBLEMS:
1. importing one component pulls in all exports (breaks tree-shaking)
2. mixing server and client components in one barrel causes build errors
3. Next.js may treat entire barrel as client if any export is client
4. TypeScript errors in unused barrel exports still cause build failures
// BAD: import from barrel
import { Button, Dialog } from "@/components";
// GOOD: import directly from source
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
GE_RULE: no barrel files (index.ts re-exports). Import directly from source file. Always.
PITFALL:USEEFFECT_FOR_DATA_FETCHING¶
SYMPTOM: loading spinners on every page. Client-side waterfalls. SEO empty pages.
FREQUENCY: extremely common with LLM-generated code (trained on pre-App-Router patterns).
ANTI_PATTERN: client component with useState + useEffect + fetch for initial page data.
// BAD: Pages Router pattern in App Router project
"use client";
import { useState, useEffect } from "react";
export default function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/products")
.then((r) => r.json())
.then(setProducts)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <ProductGrid products={products} />;
}
FIX: fetch in Server Component. Zero loading spinners. SEO gets real HTML.
// GOOD: Server Component fetches directly
import { db } from "@/lib/db";
export default async function ProductsPage() {
const products = await db.query.products.findMany();
return <ProductGrid products={products} />;
}
EXCEPTION: client-side refetching (polling, infinite scroll) — use TanStack Query.
PITFALL:CSS_SPECIFICITY_WARS¶
SYMPTOM: styles not applying. Adding !important everywhere. Unpredictable cascade.
FREQUENCY: moderate, especially when mixing Tailwind with custom CSS or third-party libraries.
ANTI_PATTERN: using !important to override Tailwind classes.
ANTI_PATTERN: writing custom CSS that competes with Tailwind utility classes.
ANTI_PATTERN: using both Tailwind classes and inline styles on the same element.
// BAD: fighting Tailwind with custom CSS
<div className="p-4" style={{ padding: "8px" }}> {/* Which padding wins? */}
FIX: use Tailwind exclusively. If custom CSS needed, use @layer to control specificity.
FIX: use cn() (tailwind-merge) to resolve class conflicts.
FIX: for third-party component styling, use data-slot attributes or CSS modules for isolation.
/* If you must write custom CSS, use @layer */
@layer components {
.custom-card {
/* These can be overridden by Tailwind utilities */
border-radius: var(--radius-lg);
padding: 1.5rem;
}
}
PITFALL:MISSING_ERROR_BOUNDARIES¶
SYMPTOM: unhandled error crashes entire page. White screen of death.
FREQUENCY: common. LLMs often generate the happy path without error handling.
ANTI_PATTERN: no error.tsx in route segments.
ANTI_PATTERN: no try/catch in Server Actions.
ANTI_PATTERN: no error handling for API calls in client components.
FIX: every route segment should have error.tsx.
FIX: every Server Action should return errors, not throw them.
FIX: wrap third-party/risky components in custom error boundaries.
// lib/actions/create-project.ts
"use server";
export async function createProject(prevState: ActionState, formData: FormData) {
try {
// ... create project
return { error: null, success: true };
} catch (error) {
// GOOD: return error, don't throw
return { error: "Failed to create project. Please try again.", success: false };
}
}
PITFALL:OVER_FETCHING_IN_CLIENT_COMPONENTS¶
SYMPTOM: fetching large datasets on the client. Slow load times. Unnecessary bandwidth.
FREQUENCY: common when converting Pages Router patterns to App Router.
ANTI_PATTERN: fetching all products then filtering on client.
// BAD: fetch 10,000 products, filter on client
"use client";
const allProducts = await fetch("/api/products").then(r => r.json());
const filtered = allProducts.filter(p => p.category === selected);
FIX: filter on the server. Fetch only what you need.
// GOOD: server-side filtering
// app/products/page.tsx (Server Component)
export default async function ProductsPage({ searchParams }) {
const { category } = await searchParams;
const products = await db.query.products.findMany({
where: category ? eq(products.category, category) : undefined,
limit: 20,
});
return <ProductGrid products={products} />;
}
PITFALL:MISSING_LOADING_STATES¶
SYMPTOM: page appears frozen during navigation. No visual feedback during data fetching.
FREQUENCY: common. LLMs focus on the final rendered state, not transitions.
FIX: add loading.tsx to every route segment that fetches data.
FIX: Suspense boundaries for independent data fetches.
FIX: Skeleton components that match final layout dimensions (prevent CLS).
PITFALL:WRONG_NEXTJS_VERSION_PATTERNS¶
SYMPTOM: code uses patterns from wrong Next.js version. Errors or unexpected behavior.
FREQUENCY: very common with LLMs trained on mixed documentation.
| Wrong Pattern | Correct Pattern (Next.js 15 + App Router) |
|---|---|
getServerSideProps |
async Server Component |
getStaticProps |
async Server Component + revalidate |
_app.tsx / _document.tsx |
app/layout.tsx |
pages/api/*.ts |
app/api/*/route.ts |
import { useRouter } from "next/router" |
import { useRouter } from "next/navigation" |
router.query |
await params or await searchParams |
getLayout pattern |
nested layout.tsx files |
getInitialProps |
Server Component + fetch |
PITFALL:TAILWIND_V3_IN_V4_PROJECT¶
SYMPTOM: classes not working. Build warnings. Deprecated utility names.
FREQUENCY: common with LLM-generated code trained on Tailwind v3.
| Tailwind v3 (WRONG) | Tailwind v4 (CORRECT) |
|---|---|
bg-gradient-to-r |
bg-linear-to-r |
flex-shrink-0 |
shrink-0 |
flex-grow |
grow |
decoration-clone |
box-decoration-clone |
tailwind.config.js |
@theme in CSS |
@apply (overuse) |
Direct utility classes |
tailwindcss-animate plugin |
tw-animate-css import |
FIX: run npx @tailwindcss/upgrade to auto-migrate.
FIX: check Tailwind v4 upgrade guide for full list of renames.
PITFALL:IGNORING_ACCESSIBILITY¶
SYMPTOM: no keyboard navigation, no screen reader support, contrast failures, no alt text.
FREQUENCY: very common with LLM-generated code. LLMs optimize for visual output, not accessibility.
TOP_MISSES:
1. <div onClick> instead of <button> — not keyboard accessible
2. Images without alt text
3. Form inputs without labels
4. Color as sole error indicator
5. Missing focus styles (outline: none everywhere)
6. No heading hierarchy
7. Modal without focus trap
8. Dynamic content not announced to screen readers
FIX: read accessibility.md before writing any component.
FIX: run axe-core in Playwright tests on every PR.
FIX: use shadcn/ui components — they have Radix accessibility built in.
PITFALL:OVER_MEMOIZATION¶
SYMPTOM: useMemo, useCallback, React.memo on everything. Code harder to read. No measurable perf gain.
FREQUENCY: moderate. Legacy React patterns before React Compiler.
ANTI_PATTERN: wrapping every function in useCallback "just in case".
ANTI_PATTERN: React.memo on every component.
// BAD: unnecessary memoization (React Compiler handles this)
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const title = useMemo(() => `Count: ${count}`, [count]);
FIX: React 19 Compiler auto-memoizes. Remove manual useMemo/useCallback unless profiling shows specific need.
FIX: if you find a performance problem, profile first (React DevTools Profiler), then optimize specifically.
LLM_SPECIFIC_MISTAKES¶
COMMON_LLM_ERRORS:
1. Generating Pages Router code for App Router project
2. Using getServerSideProps instead of async Server Component
3. Adding "use client" to every file
4. Using useEffect for data fetching
5. Importing from next/router instead of next/navigation
6. Not awaiting params and searchParams in Next.js 15+
7. Creating barrel files (index.ts exports)
8. Using Tailwind v3 class names in v4 project
9. Missing error boundaries and loading states
10. No accessibility (div for buttons, no alt text, no labels)
11. Using useFormState (old name) instead of useActionState
12. Using .errors instead of .issues on ZodError (Zod v4)
MITIGATION: every agent reads this pitfalls page before writing frontend code.
SELF_CHECK¶
BEFORE_EVERY_PR:
- [ ] no unnecessary "use client" directives?
- [ ] no barrel file imports?
- [ ] no useEffect for initial data fetching?
- [ ] no Pages Router patterns (getServerSideProps, _app.tsx)?
- [ ] error boundaries in place?
- [ ] loading states for all data fetches?
- [ ] accessibility basics (buttons, labels, alt text, contrast)?
- [ ] Tailwind v4 class names (not v3)?
- [ ] params/searchParams awaited?
- [ ] no manual memoization without profiling evidence?