How to Fix Next.js 13 App Directory 'page.js' Default Export Not a React Component Error
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Next.js 13 App Router requires
page.jsto default-export a valid React component (a function returning JSX). Exporting an object, primitive, non-JSX async function, or nothing at all causes a hard runtime crash — the route is completely unrenderable. - How to fix it: Replace the default export with a function that returns valid JSX. If the file uses async data fetching, ensure it still returns a JSX element, not raw data.
- Fast path: Use our Client-Side Sandbox above to paste your broken
page.js— it auto-refactors the default export without sending your code to any external server.
The Incident (What Does the Error Mean?)
Next.js throws one of the following at build time or runtime:
Error: The default export is not a React Component in "/app/dashboard/page"or at the React render boundary:
Error: Objects are not valid as a React child (found: object with keys {title, data}).or a blank 500 with no useful stack trace in production.
Immediate consequence: The entire route segment fails to render. Users hitting /dashboard (or whichever route) receive a 500 or a white screen. In Vercel/Edge deployments this can silently pass next build and only explode at request time if the export is async and returns a non-JSX promise resolution.
The Attack Vector / Blast Radius
This is not a security exploit — but the blast radius in a production Next.js 13 monorepo is severe:
- Cascading layout failure: The App Router wraps routes in
layout.jstrees. A brokenpage.jscan corrupt the entire layout subtree, taking down sibling routes if error boundaries are not properly scoped. - Silent CI pass:
next builddoes not always catch this. If the bad export is behind a dynamic import or a conditional, it ships to production and detonates on first real request. - Edge Runtime amplification: On Vercel Edge or Cloudflare Workers, the cold-start error is not buffered — it surfaces as a raw 500 with no retry, immediately visible to end users and crawlers, tanking Core Web Vitals and SEO signals for that route.
- Streaming SSR breakage: If you're using
React.Suspense+ streaming, a non-component default export breaks the stream mid-flight, sending a partial HTML response to the client that React cannot hydrate — producing a hydration mismatch that is extremely hard to debug.
How to Fix It
Basic Fix
The most common cause: developer exports a data object or forgets to wrap the return in JSX.
// app/dashboard/page.js
- export default {
- title: 'Dashboard',
- data: fetchDashboardData()
- }
+ export default function DashboardPage() {
+ return (
+ <main>
+ <h1>Dashboard</h1>
+ </main>
+ )
+ }Async Server Component Fix (Next.js 13 Specific)
Another common footgun — the async function fetches data but returns it raw instead of rendering it:
// app/dashboard/page.js
- export default async function DashboardPage() {
- const data = await fetch('/api/stats').then(r => r.json())
- return data // ← returns a plain object, not JSX
- }
+ export default async function DashboardPage() {
+ const data = await fetch('https://internal-api/stats', { cache: 'no-store' }).then(r => r.json())
+ return (
+ <main>
+ <h1>Stats</h1>
+ <pre>{JSON.stringify(data, null, 2)}</pre>
+ </main>
+ )
+ }Enterprise Best Practice
In large teams, enforce component return types with TypeScript and Next.js's built-in page type:
// app/dashboard/page.tsx
- export default async function DashboardPage() {
- const data = await getDashboardData()
- return data.items // array, not JSX
- }
+ import type { NextPage } from 'next'
+
+ const DashboardPage: NextPage = async () => {
+ const data = await getDashboardData()
+ return (
+ <section aria-label="Dashboard">
+ <ul>
+ {data.items.map((item) => (
+ <li key={item.id}>{item.name}</li>
+ ))}
+ </ul>
+ </section>
+ )
+ }
+
+ export default DashboardPageUsing NextPage (or NextPageProps for typed params) makes TypeScript scream at compile time if the return type is not JSX.Element | null — catching this before next build ever runs.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. TypeScript strict mode — non-negotiable in Next.js 13 monorepos:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"jsx": "preserve"
}
}2. ESLint rule to enforce valid React component exports:
// .eslintrc.json
{
"extends": ["next/core-web-vitals"],
"rules": {
"react/display-name": "error",
"react/jsx-no-useless-fragment": "warn"
}
}Add next lint as a required CI gate — it runs the above and catches missing/invalid default exports in app/**/*.{js,tsx} files.
3. Pre-commit hook with lint-staged:
// package.json
{
"lint-staged": {
"app/**/*.{js,ts,jsx,tsx}": [
"eslint --max-warnings=0",
"tsc --noEmit"
]
}
}4. GitHub Actions gate — block merge on type errors:
# .github/workflows/ci.yml
- name: Type Check
run: npx tsc --noEmit
- name: Lint
run: npx next lint --max-warnings 0
- name: Build
run: npx next buildNever let next build be the first place this is caught. By the time build runs in your pipeline, you've already wasted 3–8 minutes of CI time. TypeScript + ESLint catch this in under 10 seconds at the pre-commit stage.