How to Fix Vercel Next.js Prerendering Errors During Export: Troubleshooting React Error #321 and Page-Specific Issues
Next.js has revolutionized static site generation (SSG) and server-side rendering (SSR) for React applications, offering developers powerful tools to build fast, scalable websites. One of its most popular features is next export, which generates a fully static version of your app—ideal for hosting on platforms like Vercel, Netlify, or S3. However, prerendering errors during export can grind your deployment to a halt, leaving you staring at cryptic error messages and broken builds.
Among the most frustrating issues are React Error #321 ("Rendered more hooks than during the previous render") and page-specific prerendering failures, which often stem from mismatched server/client behavior, improper hook usage, or unhandled dynamic data. In this guide, we’ll demystify these errors, break down their root causes, and provide step-by-step solutions to get your static export back on track.
Table of Contents#
- Understanding Prerendering and Static Export in Next.js
- Common Prerendering Errors During Export
- Deep Dive: React Error #321
- Troubleshooting Page-Specific Prerendering Issues
- General Best Practices to Avoid Prerendering Errors
- Conclusion
- References
1. Understanding Prerendering and Static Export in Next.js#
Before diving into errors, let’s clarify how prerendering and next export work:
- Prerendering: Next.js generates HTML for pages at build time (for SSG) or on the fly (for ISR/SSR). This HTML is served to users, improving performance and SEO.
- Static Export (
next export): Converts your Next.js app into a static website consisting of HTML, CSS, and JavaScript files. This is possible only if your app uses SSG (viagetStaticProps/getStaticPaths) or client-side rendering (CSR) exclusively.
During export, Next.js runs a full build, prerenders all pages, and packages them into a out/ directory. Errors here typically occur when:
- Prerendering logic (e.g.,
getStaticProps) fails. - Client-side code executes unexpectedly during server prerendering.
- React’s rendering rules (e.g., hook order) are violated.
2. Common Prerendering Errors During Export#
While errors can vary, these are the most frequent culprits:
| Error Type | Description |
|---|---|
| React Error #321 | Mismatched hook counts between server prerendering and client hydration. |
| Data Fetching Failures | getStaticProps/getStaticPaths throws errors (e.g., API timeouts). |
| Client-Side API Violations | Code using window, document, or browser APIs during server rendering. |
| Environment Variable Issues | Missing or improperly exposed env vars (e.g., missing NEXT_PUBLIC_ prefix). |
| CSS-in-JS Mismatches | Styles not injected correctly during prerendering (e.g., styled-components). |
In this guide, we’ll focus on React Error #321 and page-specific issues, as they’re the hardest to debug.
3. Deep Dive: React Error #321#
What is React Error #321?#
The full error message is:
Error: Rendered more hooks than during the previous render.
React requires hooks (e.g., useState, useEffect) to be called in the same order, unconditionally, on every render. If the number of hooks called during server prerendering differs from client hydration, React throws Error #321.
Why Does It Occur During Prerendering?#
Next.js prerenders pages twice during export:
- Server Prerender: Generates static HTML (no browser APIs available).
- Client Hydration: Reconciles the server-rendered HTML with client-side React (browser APIs available).
If your component calls hooks conditionally (e.g., inside an if (window) check), the server and client may execute different code paths, leading to mismatched hook counts.
Example of Problematic Code:
// pages/index.js
import { useState, useEffect } from 'react';
export default function Home() {
const [data, setData] = useState(null);
// ❌ Conditional hook: Server skips (no window), client runs → hook count mismatch
if (typeof window !== 'undefined') {
useEffect(() => {
setData(window.localStorage.getItem('user'));
}, []);
}
return <div>{data || 'Loading...'}</div>;
} - Server Render:
windowis undefined → skipsuseEffect→ 1 hook called (useState). - Client Hydration:
windowexists → runsuseEffect→ 2 hooks called (useState+useEffect). - Result: Error #321.
Diagnosing Error #321#
To pinpoint the issue:
-
Check the Stack Trace: The error log will highlight the component and line number where the mismatch occurs.
-
Use React DevTools Profiler:
- Run
next devand enable "Highlight Updates" in DevTools. - Look for components that re-render unexpectedly during hydration.
- Run
-
Enable Debug Logs:
AddDEBUG=next:* next build && next exportto your build command to see detailed prerendering logs.
Fixes for Error #321#
Fix 1: Avoid Conditional Hook Calls#
Never wrap hooks in conditionals, loops, or nested functions. Rewrite the example above to move useEffect outside the conditional:
// ✅ Fixed: Hook called unconditionally; logic inside useEffect checks for window
export default function Home() {
const [data, setData] = useState(null);
useEffect(() => {
if (typeof window !== 'undefined') { // Check inside useEffect (client-only)
setData(window.localStorage.getItem('user'));
}
}, []);
return <div>{data || 'Loading...'}</div>;
} Fix 2: Use useEffect for Client-Only Logic#
useEffect runs after hydration, making it safe for browser APIs. For critical client-only code (e.g., initializing a library like mapbox-gl), use useEffect with an empty dependency array.
Fix 3: Validate Hook Order in Dynamic Components#
If using dynamic imports or HOCs, ensure hooks are called at the top level of the component. For example:
// ❌ Bad: Hook inside a dynamic import wrapper
const DynamicComponent = dynamic(() => import('../components/MyComponent'), { ssr: false });
function Parent() {
if (someCondition) {
return <DynamicComponent />; // Hooks in MyComponent may be skipped
}
return <div>...</div>;
}
// ✅ Good: Render DynamicComponent unconditionally, or move hooks to Parent
function Parent() {
const [isReady, setIsReady] = useState(false); // Hook at top level
useEffect(() => {
setIsReady(true);
}, []);
return isReady ? <DynamicComponent /> : null;
} 4. Troubleshooting Page-Specific Prerendering Issues#
Page-specific errors occur when a single page (e.g., /blog/[slug]) fails to prerender, breaking the entire export. Here’s how to debug them:
Step 1: Identify the Culprit Page#
Next.js logs the failing page during export. Look for lines like:
Error occurred prerendering page "/blog/my-post". Read more: https://nextjs.org/docs/messages/prerender-error
Step 2: Handle Dynamic Data and API Dependencies#
Problem: getStaticProps or getStaticPaths fails (e.g., API errors, missing data).
Solutions:
-
Add Error Handling: Wrap data fetching in
try/catchand return fallback data.// pages/blog/[slug].js export async function getStaticProps({ params }) { try { const res = await fetch(`https://api.example.com/posts/${params.slug}`); const post = await res.json(); return { props: { post } }; } catch (error) { console.error('Failed to fetch post:', error); return { props: { post: null } }; // Fallback to avoid build failure } } -
Validate
getStaticPaths: Ensurefallback: 'blocking'orfallback: falseis set correctly. For dynamic slugs, usefallback: 'blocking'to handle missing paths gracefully.
Step 3: Client-Side Only Code in Server-Rendered Components#
Problem: Code using window, document, or browser APIs (e.g., localStorage, navigator.geolocation) runs during server prerendering, causing ReferenceError.
Solutions:
-
Use
typeof window !== 'undefined'Guards:
Wrap browser code in checks to skip execution during server rendering:function MapComponent() { useEffect(() => { // Safe: useEffect runs after hydration if (typeof window !== 'undefined') { window.initMap(); // Browser API call } }, []); return <div id="map" />; } -
Dynamic Imports with
ssr: false:
For components that rely heavily on browser APIs (e.g., a charting library), import them dynamically with SSR disabled:// pages/dashboard.js import dynamic from 'next/dynamic'; // Disable SSR for this component const ClientOnlyChart = dynamic(() => import('../components/Chart'), { ssr: false }); export default function Dashboard() { return <ClientOnlyChart data={myData} />; }
Step 4: Environment Variables and Build-Time Secrets#
Problem: Missing or improperly exposed environment variables cause process.env.MY_VAR to be undefined.
Solutions:
-
Prefix Client-Side Vars with
NEXT_PUBLIC_:
Next.js only exposes env vars prefixed withNEXT_PUBLIC_to the client. For example:# .env.local NEXT_PUBLIC_API_URL=https://api.example.com # Exposed to client DB_PASSWORD=secret # Server-only (use in getStaticProps) -
Validate Vars in
getStaticProps:
Ensure server-side vars (e.g., API keys) are defined before use:export async function getStaticProps() { if (!process.env.DB_PASSWORD) { throw new Error('DB_PASSWORD is not set'); } // ... fetch data with process.env.DB_PASSWORD }
Step 5: CSS and Styling Conflicts#
Problem: CSS-in-JS libraries (e.g., styled-components, Emotion) or global CSS cause mismatches between server-rendered and client-rendered styles.
Solutions:
-
Configure CSS-in-JS for SSR:
For styled-components, installbabel-plugin-styled-componentsand update.babelrc:{ "presets": ["next/babel"], "plugins": ["styled-components"] } -
Import Global CSS in
_app.js:
Next.js requires global CSS (e.g.,styles/globals.css) to be imported in_app.js(not individual pages) to avoid prerendering issues.
5. General Best Practices to Avoid Prerendering Errors#
- Test Locally First: Always run
next build && next exportlocally before deploying to Vercel. This catches errors early. - Use TypeScript: TypeScript flags missing data or mismatched types in
getStaticProps, preventing runtime errors. - Keep Components Pure: Avoid side effects (e.g., API calls) in render logic. Use
useEffectorgetStaticPropsinstead. - Leverage ESLint: Use
eslint-plugin-react-hooksto enforce hook rules and catch conditional hook calls. - Document Data Dependencies: Track which pages rely on external APIs or dynamic data to simplify debugging.
6. Conclusion#
Prerendering errors in Next.js exports can be intimidating, but they’re rarely unsolvable. By systematically diagnosing issues—starting with the error log, validating hook usage, and isolating page-specific logic—you can resolve even the trickiest problems. Remember: React Error #321 stems from hook mismatches, while page-specific errors often involve data fetching or browser API misuse.
With the fixes and best practices outlined here, you’ll be able to export static sites confidently and deploy them seamlessly to Vercel or any static hosting platform.