Solving Common Next.js Errors: Hydration, Window & Build Issues

himmat Aug 26, 2025, 4:33 PM
Technology
Views 443
Blog Thumbnail

1) Hydration Errors

Typical message:

“Hydration failed because the initial UI does not match what was rendered on the server.”

Why it happens (in plain words):
Next.js renders HTML on the server first (SSR), then React “hydrates” it on the client. If the HTML from the server doesn’t match what the browser renders on load, React complains. Mismatches usually come from non-deterministic values (like Date.now(), Math.random(), or reading localStorage) or client-only components that tried to run on the server.

Quick checklist

  • Are you using random or time-based values in your render?

  • Are you reading browser-only APIs (window, localStorage, document) during the initial render?

  • Is some markup gated by conditions that differ between server and client?

Copy-paste fixes

  • Move browser-only logic into an effect:

     
    // ✅ Safe: runs only in the browser after first paint
    useEffect(() => {
      const theme = localStorage.getItem('theme');
      setTheme(theme ?? 'light');
    }, []);
    

     

  • Delay client-only UI until mounted:

     
    const [mounted, setMounted] = useState(false);
    useEffect(() => setMounted(true), []);
    if (!mounted) return null; // or a lightweight skeleton
    

     

  • In the App Router, if a component must be client-only, put "use client" at the top of the file.

  • When using third-party libraries that depend on the DOM, load them dynamically:

     
    const ClientOnlyWidget = dynamic(() => import('./Widget'), { ssr: false });

Pro tip:
Watch out for list keys and conditional wrappers. Changing the structure or keys between server and client (e.g., rendering a different number of <li> items) also triggers hydration errors.


2) “window is not defined” (and its cousins document, localStorage)

Why it happens:
window exists in the browser, not on the server. During SSR, any direct usage will crash.

Spot the pattern

  • Direct reads at the top level: const x = window.location.href

  • Usage inside render before mount

  • Libraries that assume a browser environment

Copy-paste fixes

  • Guard with typeof window:

     
    const isBrowser = typeof window !== 'undefined';
    const href = isBrowser ? window.location.href : '';

     

  • Move to useEffect:

     
    useEffect(() => {
      // safe browser-only work here
      console.log(window.innerWidth);
    }, []);

     

  • Dynamic import with SSR off for DOM-heavy libs:

     
    const Map = dynamic(() => import('./Map'), { ssr: false });
  • App Router: Mark the component as client:

    "use client";
    // component code that touches window
    Pro tip:
    Sometimes a deep dependency uses window. Load just that widget with ssr:false instead of turning your whole page into client mode.

3) Build Errors (next build / production surprises)

a) “Module not found / Can’t resolve …”

  • Cause: Wrong import path, file renamed, or case mismatch (import x from './Logo' vs ./logo on case-sensitive systems).

  • Fix: Confirm the path, check casing, ensure the file actually exists in prod (Git sometimes ignores case-only changes).

b) TypeScript or ESLint stops the build

  • Cause: Strict configs catch what dev hot-reload lets slide.

  • Fix: Read the first error (others are often symptoms). Either fix the type, add a type guard, or adjust tsconfig.json/.eslintrc to match your team’s tolerance. Avoid blanket any; prefer narrow type assertions.

c) Environment variable gotchas

  • Symptoms: Works locally, fails in prod; values are undefined.

  • Fixes:

    • Put secrets in .env.local (dev) and your hosting provider’s env panel for prod.

    • Client-side usage must start with NEXT_PUBLIC_:

      NEXT_PUBLIC_API_URL=https://api.example.com
    • After changing envs, restart the server; rebuild for production.

d) Image & asset hiccups

  • next/image external domains: Add them to next.config.js:

    images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }
  • Static assets: Place under /public and reference /my-file.png.

e) Node / package mismatches

  • Cause: Using a Node version outside Next’s supported range, or mismatched React/Next versions.

  • Fix: Use the current LTS Node for your project. Delete node_modules + lockfile, then reinstall:

     
    rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
    npm i   # or yarn / pnpm

f) “Cannot use import statement outside a module”

  • Cause: Config conflict (ESM vs CJS) in a dependency or tool script.

  • Fix: Ensure your type in package.json matches your code style ("type":"module" for ESM). For server utils that need CJS, use .cjs extensions or update the import syntax accordingly.


A Calm Debugging Flow (save this!)

  1. Reproduce in a clean state: Stop dev server, clear cache, restart. Try next build to see the real error stack.

  2. Minimal example: Comment out sections until the error disappears. The last change points to the culprit.

  3. Server vs client: Ask, “Does this line need the browser?” If yes, move it to useEffect or a client-only component.

  4. Determinism check: Remove randomness/time from the initial render. Replace with placeholders and fill after mount.

  5. One fix at a time: Change, test, commit. Don’t blend three fixes—you’ll never know which one worked.


Copy-Ready Snippets

Client-only wrapper:

 
"use client";
export default function ClientOnly({ children }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return mounted ? children : null;
}

Dynamic import for DOM libraries:

 
const Chart = dynamic(() => import('./Chart'), { ssr: false });

Safe localStorage read:

 
const [theme, setTheme] = useState('light');
useEffect(() => {
  try {
    const saved = localStorage.getItem('theme');
    if (saved) setTheme(saved);
  } catch {}
}, []);

 


Final Thoughts

Next.js errors feel scary until you see the pattern: What ran on the server that should have waited for the browser? or What changed between server and client? Apply the guards above, keep your first render deterministic, and lean on dynamic imports for DOM-heavy pieces. With that mindset, hydration warnings, window blow-ups, and stubborn build breaks turn from blockers into quick fixes.

If you want, share the specific error text and a small code snippet—I’ll tailor the exact fix to your setup.

Comments

Please login to leave a comment.

No comments yet.

Related Posts