This error occurs when React.lazy() imports a module that lacks a default export. React.lazy() only supports default exports; if your module uses named exports, you must create a wrapper to re-export them as default, or use promise chaining to transform named exports into the default export structure.
The "React.lazy promise did not resolve to a default component" error indicates that the dynamic import in your React.lazy() function resolved to a module that doesn't have a default export, or the resolved value isn't a valid React component. React.lazy() is designed to work with dynamic imports that resolve to modules with a default export. When you call lazy(() => import('./Component')), React expects the promise to resolve with a module object that has a .default property containing a React component (either a function component or class component). The error occurs in two scenarios: 1. The imported module uses named exports instead of a default export (e.g., export function MyComponent {}) 2. The promise resolves to undefined or an invalid component type This is an intentional design limitation—React.lazy() doesn't automatically unwrap named exports. It strictly expects modules with default exports to ensure proper tree-shaking and bundling behavior.
First, check that the component file you're importing exports a React component as the default export.
Correct:
// MyComponent.js
export default function MyComponent() {
return <div>Hello</div>;
}Or with named export converted to default:
// MyComponent.js
export function MyComponent() {
return <div>Hello</div>;
}Incorrect (will cause error):
// MyComponent.js
export function MyComponent() {
return <div>Hello</div>;
}
// No default export!Ensure the component file explicitly exports something as export default at the module level.
If the module uses named exports but you can't change it, create a wrapper module that re-exports it as default.
Create MyComponentWrapper.js:
// MyComponentWrapper.js
export { MyComponent as default } from './MyComponent';Then use lazy with the wrapper:
import { lazy, Suspense } from 'react';
const MyComponent = lazy(() => import('./MyComponentWrapper'));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}This approach maintains proper tree-shaking and bundling while allowing lazy loading of named exports.
Alternatively, transform named exports into the expected module structure using promise chaining:
import { lazy, Suspense } from 'react';
const MyComponent = lazy(() =>
import('./MyComponent').then(module => ({
default: module.MyComponent
}))
);
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}The .then() callback receives the imported module and returns an object with a default property containing the component. React.lazy() then receives the expected structure.
For multiple named exports:
const ComponentA = lazy(() =>
import('./Components').then(m => ({ default: m.ComponentA }))
);
const ComponentB = lazy(() =>
import('./Components').then(m => ({ default: m.ComponentB }))
);The dynamic import must resolve to a module where the default property is a React component (function or class).
Valid:
// Button.js
export default function Button() {
return <button>Click me</button>;
}
// Usage
const Button = lazy(() => import('./Button'));Valid (class component):
// FormComponent.js
export default class FormComponent extends React.Component {
render() {
return <form>...</form>;
}
}
// Usage
const FormComponent = lazy(() => import('./FormComponent'));Invalid:
// BadComponent.js
export default {
name: 'MyComponent',
render: () => <div>Hello</div>
};
// This will fail—the default export must be a React component, not an object
const BadComponent = lazy(() => import('./BadComponent'));Verify that the default export is a callable function or class, not a plain object or other value.
Verify that the path passed to import() is correct and your build tool (webpack, Vite, etc.) properly resolves dynamic imports.
Correct:
const MyComponent = lazy(() => import('./components/MyComponent'));
const Other = lazy(() => import('@/components/Dialog')); // With path aliasesIncorrect:
const MyComponent = lazy(() => import('./NonExistentFile')); // Path doesn't existFor path aliases (like @/), ensure your build config supports them:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});Or in webpack:
// webpack.config.js
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/'),
},
},
};Check the browser's Network tab to confirm the chunk is loading, or use your dev tools to verify the import resolution.
Always wrap lazy components in a Suspense boundary with a fallback UI. This prevents errors if the chunk fails to load or resolution is delayed:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export default function App() {
return (
<Suspense fallback={<div className="spinner">Loading component...</div>}>
<HeavyComponent />
</Suspense>
);
}The fallback UI is shown while the chunk loads. If the promise rejects, the nearest Error Boundary catches it:
import { lazy, Suspense, Component } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Failed to load component. Please refresh the page.</div>;
}
return this.props.children;
}
}
export default function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}If you're still seeing the error, log the imported module to verify its structure:
const MyComponent = lazy(() =>
import('./MyComponent').then(module => {
console.log('Imported module:', module);
console.log('Default export:', module.default);
if (!module.default) {
console.error('No default export found. Available exports:', Object.keys(module));
}
return module;
})
);This logs the module object and all available exports. Check the console to see:
- If module.default exists and is a function/class
- What other exports are available if default is missing
- Whether the module is being imported at all
Then adjust your lazy() call based on what you find:
// If you see named exports like MyComponent, use:
const MyComponent = lazy(() =>
import('./MyComponent').then(m => ({ default: m.MyComponent }))
);
// If it's undefined, check the file path or export statementTree-Shaking and Bundling: The reason React.lazy() requires default exports is to preserve tree-shaking. When your bundler (webpack, Vite) encounters a dynamic import with a default export, it can more reliably create a separate chunk for just that module. Named exports complicate this analysis, which is why the wrapper pattern is recommended over complex promise chains—it keeps the module boundary clean and explicit.
Next.js Dynamic Imports: If you're using Next.js, prefer next/dynamic over React.lazy() for server-side considerations:
import dynamic from 'next/dynamic';
const MyComponent = dynamic(() => import('./MyComponent'), {
loading: () => <div>Loading...</div>,
ssr: false, // Disable server-side rendering if needed
});
export default function Page() {
return <MyComponent />;
}Error Boundary Placement: Lazy component failures (network errors, invalid modules) should be caught by an Error Boundary, not Suspense. Suspense handles loading states; Error Boundary handles errors.
Bundle Analysis: Use tools like webpack-bundle-analyzer to verify that lazy imports create separate chunks:
npm install --save-dev webpack-bundle-analyzerThis helps confirm that your dynamic imports are being split into separate files as intended, which is the main benefit of code splitting with lazy loading.
Module Federation: In Micro Frontend architectures, lazy components across module boundaries require additional configuration to handle remote entry points. Ensure the remote module exports a default component and that your host app's webpack/Vite config properly handles federated imports.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite