Skip to main content
Next.js, despite its powerful features and optimizations, has several common anti-patterns that can lead to performance issues, maintenance problems, and poor user experience. Here are the most important anti-patterns to avoid when building Next.js applications.
// Anti-pattern: Using client-side data fetching for static content
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function fetchProduct() {
      const res = await fetch(`/api/products/${productId}`);
      const data = await res.json();
      setProduct(data);
      setLoading(false);
    }
    
    fetchProduct();
  }, [productId]);
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

// Better approach: Use getStaticProps for static content
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`);
  const product = await res.json();
  
  return {
    props: {
      product,
    },
    revalidate: 60, // Optional: Regenerate page after 60 seconds
  };
}

export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  
  const paths = products.map((product) => ({
    params: { id: product.id.toString() },
  }));
  
  return { paths, fallback: 'blocking' };
}

function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
Using client-side data fetching for content that could be pre-rendered leads to poor performance and SEO. Choose the appropriate data fetching method: getStaticProps for static content, getServerSideProps for dynamic content, and client-side fetching only for user-specific or frequently changing data.
// Anti-pattern: Using regular img tags
function ProductCard({ product }) {
  return (
    <div className="card">
      <img 
        src={product.imageUrl} 
        alt={product.name} 
        width={300} 
        height={200} 
      />
      <h3>{product.name}</h3>
    </div>
  );
}

// Better approach: Use Next.js Image component
import Image from 'next/image';

function ProductCard({ product }) {
  return (
    <div className="card">
      <Image 
        src={product.imageUrl} 
        alt={product.name} 
        width={300} 
        height={200} 
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,..."
        priority={product.featured}
      />
      <h3>{product.name}</h3>
    </div>
  );
}
Regular <img> tags don’t benefit from Next.js’s automatic image optimization. Use the next/image component to get automatic image optimization, lazy loading, and proper sizing.
// Anti-pattern: Using API routes to fetch data for getStaticProps
// pages/products/[id].js
export async function getStaticProps({ params }) {
  // Don't do this - it creates an unnecessary network request
  const res = await fetch(`http://localhost:3000/api/products/${params.id}`);
  const product = await res.json();
  
  return {
    props: { product },
  };
}

// pages/api/products/[id].js
export default async function handler(req, res) {
  const { id } = req.query;
  const product = await getProductFromDatabase(id);
  res.status(200).json(product);
}

// Better approach: Import the data fetching logic directly
// lib/products.js
export async function getProductById(id) {
  return await getProductFromDatabase(id);
}

// pages/products/[id].js
import { getProductById } from '../../lib/products';

export async function getStaticProps({ params }) {
  const product = await getProductById(params.id);
  
  return {
    props: { product },
  };
}

// pages/api/products/[id].js
import { getProductById } from '../../../lib/products';

export default async function handler(req, res) {
  const { id } = req.query;
  const product = await getProductById(id);
  res.status(200).json(product);
}
Using API routes to fetch data for getStaticProps or getServerSideProps creates unnecessary network requests. Import the data fetching logic directly in both server-side functions and API routes.
// Anti-pattern: Using getServerSideProps for semi-static content
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  
  return {
    props: { products },
  };
}

// Better approach: Use Incremental Static Regeneration
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  
  return {
    props: { products },
    revalidate: 60, // Regenerate page after 60 seconds if requested
  };
}
Using getServerSideProps for content that doesn’t change frequently adds unnecessary server load. Use Incremental Static Regeneration (ISR) with revalidate for content that changes occasionally.
// Anti-pattern: Loading large JavaScript libraries upfront
import LargeChart from 'heavy-chart-library';

function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <LargeChart data={someData} />
    </div>
  );
}

// Better approach: Use dynamic imports and suspense
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const LargeChart = dynamic(() => import('heavy-chart-library'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Disable server-side rendering if the library isn't SSR compatible
});

function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading chart...</div>}>
        <LargeChart data={someData} />
      </Suspense>
    </div>
  );
}
Loading large JavaScript libraries upfront hurts performance metrics like Largest Contentful Paint (LCP) and Time to Interactive (TTI). Use dynamic imports, code splitting, and prioritize loading critical content first.
// Anti-pattern: Using plain JavaScript without type checking
function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.address.street}, {user.address.city}</p>
    </div>
  );
}

// Better approach: Use TypeScript
interface Address {
  street: string;
  city: string;
  zipCode: string;
  country: string;
}

interface User {
  id: string;
  name: string;
  email: string;
  address: Address;
}

interface UserProfileProps {
  user: User;
}

function UserProfile({ user }: UserProfileProps) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.address.street}, {user.address.city}</p>
    </div>
  );
}
Not using TypeScript can lead to runtime errors and makes refactoring more difficult. Use TypeScript to catch errors at compile time and improve code maintainability.
// Anti-pattern: Duplicating layout code in every page
function HomePage() {
  return (
    <div>
      <header>
        <nav>{/* Navigation items */}</nav>
      </header>
      <main>
        <h1>Welcome to our site</h1>
        {/* Page content */}
      </main>
      <footer>
        {/* Footer content */}
      </footer>
    </div>
  );
}

function AboutPage() {
  return (
    <div>
      <header>
        <nav>{/* Navigation items */}</nav>
      </header>
      <main>
        <h1>About Us</h1>
        {/* Page content */}
      </main>
      <footer>
        {/* Footer content */}
      </footer>
    </div>
  );
}

// Better approach: Use layout components
function MainLayout({ children }) {
  return (
    <div>
      <header>
        <nav>{/* Navigation items */}</nav>
      </header>
      <main>{children}</main>
      <footer>
        {/* Footer content */}
      </footer>
    </div>
  );
}

function HomePage() {
  return (
    <MainLayout>
      <h1>Welcome to our site</h1>
      {/* Page content */}
    </MainLayout>
  );
}

function AboutPage() {
  return (
    <MainLayout>
      <h1>About Us</h1>
      {/* Page content */}
    </MainLayout>
  );
}

// In Next.js 13+ with App Router
// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>{/* Navigation items */}</nav>
        </header>
        <main>{children}</main>
        <footer>
          {/* Footer content */}
        </footer>
      </body>
    </html>
  );
}
Duplicating layout code across pages leads to maintenance issues. Use layout components or the App Router’s layout system to share common UI elements across pages.
// Anti-pattern: Not configuring Next.js properly
// next.config.js
module.exports = {
  // No configuration
};

// Better approach: Configure Next.js properly
// next.config.js
module.exports = {
  images: {
    domains: ['images.example.com'], // Allow images from this domain
    formats: ['image/avif', 'image/webp'], // Specify image formats
  },
  i18n: {
    locales: ['en', 'fr', 'es'],
    defaultLocale: 'en',
  },
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true,
      },
    ];
  },
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
        ],
      },
    ];
  },
};
Not configuring Next.js properly means missing out on important optimizations and features. Use the Next.js config file to configure image domains, internationalization, redirects, headers, and other important settings.
// Anti-pattern: No error handling
function ProductPage({ product }) {
  return (
    <div>
      <Header />
      <ProductDetails product={product} /> {/* If this crashes, the whole page crashes */}
      <RelatedProducts ids={product.relatedIds} />
      <Footer />
    </div>
  );
}

// Better approach: Use error boundaries
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function ProductPage({ product }) {
  return (
    <div>
      <Header />
      <ErrorBoundary 
        FallbackComponent={ErrorFallback}
        onReset={() => {
          // Reset the state of your app here
        }}
      >
        <ProductDetails product={product} />
      </ErrorBoundary>
      <RelatedProducts ids={product.relatedIds} />
      <Footer />
    </div>
  );
}
Without error boundaries, a runtime error in a component can break the entire page. Use error boundaries to gracefully handle errors and display fallback UIs.
// Anti-pattern: Using client components for everything in Next.js 13+
'use client';

import { useState } from 'react';

async function getData() {
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

export default function Page() {
  const [count, setCount] = useState(0);
  const data = await getData(); // This will error in client components
  
  return (
    <div>
      <h1>Data: {data.title}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Better approach: Split into server and client components
// Page.js (Server Component)
import { Counter } from './Counter';

async function getData() {
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

export default async function Page() {
  const data = await getData();
  
  return (
    <div>
      <h1>Data: {data.title}</h1>
      <Counter />
    </div>
  );
}

// Counter.js (Client Component)
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
Using client components for everything in Next.js 13+ misses the benefits of server components. Use server components for data fetching and rendering static content, and client components only for interactive parts of your UI.
// Anti-pattern: Not implementing proper caching
// pages/products/[id].js
export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`);
  const product = await res.json();
  
  return {
    props: { product },
  };
}

// Better approach: Implement proper caching
// pages/products/[id].js
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`, {
    headers: {
      'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=59',
    },
  });
  const product = await res.json();
  
  return {
    props: { product },
    revalidate: 60,
  };
}

// In Next.js 13+ App Router
// app/products/[id]/page.js
export const revalidate = 60; // Revalidate at most every 60 seconds

async function getProduct(id) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 60 },
  });
  return res.json();
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
Not implementing proper caching strategies can lead to unnecessary API calls and slower performance. Use appropriate caching headers and Next.js’s built-in caching mechanisms like ISR and the fetch API’s cache options.
I