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 DashboardPage
Using 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 build
Never 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.