Common React to Next.js Migration Issues (And Fixes)

Real problems from actual migrations: window is not defined, CSS not loading, environment variables breaking. Here's what went wrong and how to fix it.

Migrating from React to Next.js can transform your application's performance and SEO, but the journey isn't without challenges. After helping hundreds of developers with their migrations, we've identified the most common pitfalls—and more importantly, how to avoid them.

1. Forgetting About Server vs. Client Components

The Problem

In Next.js 13+, components are server components by default. Using browser APIs like window, localStorage, or event handlers without the 'use client' directive causes errors.

// This will fail in Next.js export default function MyComponent() { const width = window.innerWidth; // Error! return <div>Width: {width}</div>; }

The Solution

Add 'use client' at the top of components that use browser APIs or React hooks like useState and useEffect.

// Correct approach 'use client'; export default function MyComponent() { const [width, setWidth] = useState(0); useEffect(() => { setWidth(window.innerWidth); }, []); return <div>Width: {width}</div>; }

2. Incorrect Image Optimization Usage

The Problem

Using regular <img> tags instead of Next.js's optimized Image component, or forgetting to specify width and height properties.

The Solution

Always use Next.js's Image component with proper dimensions. This enables automatic optimization, lazy loading, and modern format conversion.

// Correct approach import Image from 'next/image'; export default function Hero() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // For above-the-fold images /> ); }

3. Not Handling Dynamic Imports Properly

The Problem

Large third-party libraries that only work in the browser (like charts or maps) bloat your initial bundle and cause SSR errors.

The Solution

Use Next.js's dynamic imports with ssr: false for client-only libraries.

// Correct approach import dynamic from 'next/dynamic'; const Chart = dynamic(() => import('react-chartjs-2'), { ssr: false, loading: () => <p>Loading chart...</p> }); export default function Dashboard() { return <Chart data={chartData} />; }

4. Misunderstanding Data Fetching Patterns

The Problem

Using useEffect for all data fetching, missing out on Next.js's powerful server-side data fetching capabilities.

The Solution

Use server components for data fetching when possible. They're faster, more secure, and better for SEO.

// Server component with async data fetching async function getProducts() { const res = await fetch('https://api.example.com/products'); return res.json(); } export default async function ProductList() { const products = await getProducts(); return ( <div> {products.map(product => ( <ProductCard key={product.id} {...product} /> ))} </div> ); }

5. Ignoring Metadata and SEO Configuration

The Problem

Forgetting to migrate React Helmet or other SEO solutions to Next.js's metadata API, resulting in poor SEO.

The Solution

Use Next.js 13+ metadata API for static and dynamic SEO tags.

// app/products/[id]/page.js export async function generateMetadata({ params }) { const product = await getProduct(params.id); return { title: product.name, description: product.description, openGraph: { images: [product.image], }, }; }

6. Not Configuring Environment Variables Correctly

The Problem

Exposing sensitive API keys or not understanding the difference between server and client environment variables.

The Solution

Use NEXT_PUBLIC_ prefix only for client-side variables. Keep sensitive keys server-side only.

# .env.local # Server-only (secure) DATABASE_URL=postgresql://... API_SECRET_KEY=secret123 # Solution: Client-accessible (public) NEXT_PUBLIC_API_URL=https://api.example.com

7. Incorrect Routing Migration

The Problem

Trying to use React Router patterns in Next.js or not understanding file-based routing conventions.

The Solution

Embrace Next.js's file-based routing. Your folder structure becomes your URL structure.

// React Router pattern <Route path="/products/:id" component={ProductDetail} /> // Solution: Next.js equivalent // Create: app/products/[id]/page.js export default function ProductDetail({ params }) { return <div>Product {params.id}</div>; }

8. Overlooking CSS and Styling Migration

The Problem

Global CSS imports in components causing style conflicts or build errors.

The Solution

Use CSS Modules for component styles or import global CSS only in the root layout.

// Solution: CSS Modules approach import styles from './Button.module.css'; export default function Button({ children }) { return <button className={styles.button}>{children}</button>; } // Solution: Global CSS in app/layout.js only import './globals.css';

9. Not Testing for Hydration Errors

The Problem

Server-rendered HTML doesn't match client-rendered HTML, causing hydration mismatches and console errors.

The Solution

Avoid rendering different content on server vs. client. Use useEffect for client-only content.

// Correct approach for client-only content 'use client'; export default function UserGreeting() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) return null; return <div>Welcome, {localStorage.getItem('username')}</div>; }

10. Forgetting About API Route Security

The Problem

Exposing API routes without proper authentication or rate limiting.

The Solution

Always validate requests and implement proper security measures in API routes.

// app/api/data/route.js import { NextResponse } from 'next/server'; export async function POST(request) { // Validate authentication const token = request.headers.get('authorization'); if (!token) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Validate request body const body = await request.json(); if (!body.email) { return NextResponse.json({ error: 'Email required' }, { status: 400 }); } // Process request... return NextResponse.json({ success: true }); }

Avoid These Pitfalls Automatically

ReactToNext handles many of these common issues automatically, helping you migrate faster and with fewer errors.

Try ReactToNext Free →

Conclusion

Migrating from React to Next.js doesn't have to be painful. By understanding these common pitfalls and their solutions, you can avoid the mistakes that trip up most developers. Remember:

With careful planning and the right tools, your migration can be smooth and successful. Start small, test often, and don't hesitate to use automation tools to handle the repetitive work.