Skip to content

Element type is invalid: expected a string, got undefined

Element type is invalid: expected a string but got undefined? Eight causes, ranked, with the one line of output that tells them apart and the fix that sticks.

React reactjs es-modules imports debugging
BKG
Reading Progress

On This Page

Uncaught Error: Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined. You likely forgot
to export your component from the file it's defined in, or you might have mixed up
default and named imports.

Check the render method of `App`.

That's the development build. The production build says Minified React error #130 and nothing else, which is why half the people who hit this in production can't even tell which of the five variants they have.

Element type is invalid means one thing: somewhere you wrote <Something />, and Something isn't a string, a function, a class, or one of React's wrapper objects. It is undefined most of the time. Sometimes it's object, boolean, null, or an array. Each word after "got:" points at a different mistake, and almost nobody reads it.

This took me more than a day to pin down properly, mostly because my first three repros refused to fail the way production does. Below: every variant I could produce on React 18.3.1 and 19.0 through 19.3, the eight causes ranked, a way to find the bad import in under ten minutes, and the build settings that turn this runtime crash into a failed build.

The message, and its production twin

Development, React 18.3.1 and 19.x, the undefined flavour (dev builds, via createFiberFromTypeAndProps in react-dom):

Error: Element type is invalid: expected a string (for built-in components) or a
class/function (for composite components) but got: undefined. You likely forgot to
export your component from the file it's defined in, or you might have mixed up
default and named imports.

Check the render method of `App`.

The object flavour, when the object has keys (a module namespace, or a CommonJS exports with things in it). There is no "you likely forgot" sentence this time:

Error: Element type is invalid: expected a string (for built-in components) or a
class/function (for composite components) but got: object.

Check the render method of `App`.

The object flavour with the hint attached means the object is empty, {}:

...but got: object. You likely forgot to export your component from the file it's
defined in, or you might have mixed up default and named imports.

The other types I produced:

...but got: boolean.
...but got: null.
...but got: <section />. Did you accidentally export a JSX literal instead of a component?

And the lazy cousin, which has its own wording and its own production code:

Error: Element type is invalid. Received a promise that resolves to: undefined.
Lazy element type must resolve to a class or function.

In a production build all of these collapse into numbers. React 18.3.1:

Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invariant=130&args[]=undefined&args[]= for the full message...

React 19.x, same error, newer decoder host:

Minified React error #130; visit https://react.dev/errors/130?args[]=undefined&args[]= for the full message...

Read that URL. args[]=undefined is the got: value. The second, empty args[]= is where the "Check the render method of" line would go. In production it's always blank, so you lose the component name. The lazy variant is #306, with the same two args.

One more thing worth knowing before you start digging: React 18.3.1 and 19.0.7 print a separate console line first, React.jsx: type is invalid -- expected a string…, when the element is created. React 19.1.10, 19.2.7 and 19.3.1 do not (in my runs; I counted the lines). If your logs show that extra warning, you're on the older line.

Break it on purpose, one file per cause

I ran everything through one harness: esbuild bundles a tiny App per case, jsdom provides the DOM, createRoot renders it inside an error boundary, and the script prints whatever the boundary caught. One folder per React version, each with its own node_modules. Setup (Node 22):

mkdir v19 && cd v19 && npm init -y
npm i react@19.3.1 react-dom@19.3.1 esbuild@0.28.2 jsdom@25.0.1
# repeat in v18 (react@18.3.1), v190 (19.0.7), v19.1 (19.1.10), v19.2 (19.2.7)

The runner (run.cjs), abbreviated to the part that matters:

// run.cjs  usage: node run.cjs c3-compound   (PROD=1 for a production run)
const esbuild = require("esbuild");
const { JSDOM } = require("jsdom");
const path = require("path");
const prod = process.env.PROD === "1";
if (prod) process.env.NODE_ENV = "production"; // must be set BEFORE react loads

(async () => {
  const c = process.argv[2];
  const out = path.join(process.cwd(), `out-${c}.cjs`); // inside THIS version's folder
  require("fs").writeFileSync("entry.jsx", `
    import React from "react";
    import { createRoot } from "react-dom/client";
    import App from "./src/${c}.jsx";
    class EB extends React.Component {
      state = { e: null };
      static getDerivedStateFromError(e) { globalThis.__msg = e.message; return { e }; }
      render() { return this.state.e ? "ERR" : this.props.children; }
    }
    export const go = () => createRoot(document.getElementById("root")).render(<EB><App /></EB>);
  `);
  await esbuild.build({
    entryPoints: ["entry.jsx"], bundle: true, outfile: out, format: "cjs", platform: "node",
    jsx: "automatic", loader: { ".jsx": "jsx" }, logLevel: "silent",
    external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"],
  });
  const dom = new JSDOM('<div id="root"></div>');
  global.window = dom.window; global.document = dom.window.document;
  require(out).go();
  await new Promise((r) => setTimeout(r, 150));
  console.log(`== ${c}: ${globalThis.__msg}`);
})();

Now the case files under src/. The comment on each marks the line that matters.

// src/lib-named.cjs      (a CommonJS library with a NAMED export only)
exports.Card = function Card() { return null; };

// src/c1-named-vs-default.jsx
import Card from "./lib-named.cjs"; // default import of a module with no default
export default function App() { return <Card />; }
// src/c3-compound.jsx
function Tabs({ children }) { return <div>{children}</div>; }
Tabs.List = function List() { return <ul />; };
// Tabs.Panel is never assigned
export default function App() { return <Tabs><Tabs.List /><Tabs.Panel /></Tabs>; }
// src/Home.cjs
const { routes } = require("./routes.cjs"); // cycle: routes.cjs requires this file back
exports.Home = function Home() { return null; };

// src/routes.cjs
const { Home } = require("./Home.cjs");     // Home.cjs is still half-loaded: this is undefined
exports.routes = [{ path: "/", component: Home }];

// src/c11-cjs-cycle.jsx
const { Home } = require("./Home.cjs");
const { routes } = require("./routes.cjs");
export default function App() { const R = routes[0].component; return <R />; }
// src/c4-namespace.jsx
import * as Card from "./Card.jsx"; // Card.jsx: export function Card() {...}
export default function App() { return <Card />; } // Card is the namespace object

// src/c5-false.jsx
export default function App({ heading = false }) {
  const H = heading && "h2";            // false, not "h2"
  return <H>title</H>;
}

// src/c6-lazy.jsx
import { lazy, Suspense } from "react";
const L = lazy(() => import("./lib-named.cjs")); // resolves to an object with no default
export default function App() { return <Suspense fallback="loading"><L /></Suspense>; }

Run any of them: node run.cjs c3-compound. Confirmation signal: the line == c3-compound: Element type is invalid: … got: undefined. You likely forgot to export…. If you instead see got: object. with no "you likely forgot" sentence, you've hit cause 5 (a namespace or CJS shape), not cause 1. Delete out-*.cjs and entry.jsx when you're done; nothing was installed globally.

What changed from 18.3 to 19.3

Mostly nothing about the core message. That's the useful part: the thing you learn here transfers across majors. The differences are in the margins.

Case18.3.119.0.719.1.10 / 19.2.7 / 19.3.1
Missing property, <Tabs.Panel />throws, got: undefined + hintsamesame
Separate React.jsx: type is invalid console lineyesyesno
export default <section/>got: object.got: <section />. Did you accidentally export a JSX literal…same as 19.0.7
memo(undefined)TypeError: Cannot read properties of undefined (reading 'propTypes')Element type is invalid… got: undefinedsame as 19.0.7
lazy() with no defaultsame message; dev warning shows [object Object]warning prints { Card: [Getter] }same
Production code#130 / #306, reactjs.org decoder#130 / #306, react.dev decodersame

I ran the memo and lazy-warning rows on 18.3.1, 19.0.7 and 19.3.1 only. The memo row bit me. On 18.3.1, wrapping an undefined import in memo() doesn't give you the friendly message at all. You get a TypeError about propTypes thrown from somewhere that looks unrelated, plus a console warning memo: The first argument must be a component. Instead received: undefined. If your error says propTypes, the culprit is the argument to memo or forwardRef, and the hunt below applies unchanged.

Frameworks don't change the React side of this. I didn't reproduce the Next.js Server Component version of the mistake (importing something that isn't a component across the boundary), so I'm leaving that out rather than guess at its wording.

A variable held something that isn't a component

The short answer: React was handed a value in the type position of an element, and the value wasn't renderable as one. <Card /> compiles to jsx(Card, props). Whatever Card evaluates to at that moment is what React inspects. A string is a DOM tag. A function or class is a component. memo, forwardRef, lazy and context objects are recognised by a tag React puts on them. Anything else is this error.

So the bug is never in the JSX. It is in how Card got its value, and that is decided by the module system, before React runs at all. Two layers can be responsible. Your bundler or runtime resolved the import to something unexpected (cases 1, 2, 3, 5, 7). Or your own code computed a bad value (cases 4, 6, 8).

Here is the part that makes this error painful in practice: nothing warned you. In my esbuild run the missing default export was a hard build error. Vite 8.3.5 refused too. But webpack 5.112.2 compiled the same file, exited with status 0, and shipped a bundle in which the import was undefined. I'll show the output later. If your toolchain is webpack-shaped, or the import comes from a CommonJS file, the first time anyone hears about the mistake is a user's console.

Where Element type is invalid gets thrown, and why the hint goes missing

In react-dom's development bundle the throw lives in createFiberFromTypeAndProps (in the source tree that's ReactFiber.js). React walks the element's type: string means a host element, function means a component, and an object is checked for $$typeof against forward-ref, memo, lazy, context and a few others. If nothing matches, it builds the error. I read the 19.3.1 bundle to see how the message is assembled, and one condition explains a lot of confusion:

The "You likely forgot to export…" sentence is appended only when type === undefined, or when type is an object with zero own keys. That's it. It's a guess about your mistake, and React makes it only in those two situations.

This is a free diagnostic. Three readings:

  • got: undefined + hint: the binding is missing entirely. Wrong name, wrong default/named choice, a cycle, or a property that was never assigned.
  • got: object + hint: you imported an empty object. In my runs that was a module.exports = {}, which is also exactly what a half-loaded CommonJS module looks like to its cyclic importer.
  • got: object with no hint: the object has keys. You imported a whole module (import * as, or a CJS exports containing the component under a different property).

19.x adds two branches for readability. An array becomes got: array, and a React element becomes got: <section />. with "Did you accidentally export a JSX literal instead of a component?". Booleans and numbers fall through to typeof. The owner line ("Check the render method of App") is attached when React knows which component's render created the element, and only in development. Which is why production always has the second args[]= empty.

One analogy, since the whole thing hangs on it: the type slot is a function call position. Card() where Card is undefined would throw Card is not a function, and you'd go straight to the import. React defers the same failure until it reaches the element and then rewords it, so the trail to the import is less obvious than it should be.

Which import is the empty one?

Three things I tried first, none of which worked.

Reproducing the named/default mismatch with plain ESM. I wrote import Card from "./Card.jsx" against a file with only export function Card, ran the esbuild harness, and got:

✘ [ERROR] No matching export in "src/Card.jsx" for import "default"

Build failure, no runtime error, nothing to debug. I dropped ESM and used a CommonJS library file (exports.Card = …), where a default import silently gets the whole exports object. That's the realistic version anyway: the app imports a dependency, the dependency's published format is the problem.

Reproducing a circular import with ES modules. Home.jsx and routes.jsx importing each other rendered fine: root text: "home (1 routes)". esbuild hoists function declarations, so the binding existed by the time anything read it. To get the production behaviour I had to use CJS require, with the require of the cycle partner placed before exports.Home = …. Only then did routes[0].component come out undefined. If your cycle uses const Home = () => … instead, you'll get the temporal dead zone error rather than this one, which has its own write-up in ReferenceError: Cannot access before initialization.

Trusting my version matrix. My first run printed the right messages on all five versions, which should have worried me, because 18.3.1 and 19.3.1 don't print identical output. The at beginWork (…) frames in one result pointed at /opt/npm-tools/node_modules/react-dom/, not at the v18 folder. esbuild had written its output file one directory up, where require("react") resolved to a global copy. Every "version" was the same React. Fix: write the bundle inside each version's own folder (the out line in the runner above) and print require("react/package.json").version first. If you ever run this kind of matrix, check the stack paths before you trust a single line of it.

Now the path for your own app. Run these in order.

# 1. Is it a component you wrote, or a library's?
#    The dev message names the owner: "Check the render method of `X`".
#    Open X, find every JSX tag in it. One of them is the culprit.

# 2. Which tag? Add a temporary line above the return of component X:
#      console.log({ Card, Tabs, "Tabs.Panel": Tabs.Panel });
#    Whichever prints undefined, {} or [Module] is your answer.

# 3. Is it a package, and did the version just change?
npm ls some-ui-lib
git diff package-lock.json | grep -A2 some-ui-lib
#    A major bump that renamed or removed an export is cause 2.

# 4. Check what the package really exports at runtime:
node -e "console.log(Object.keys(require('some-ui-lib')))"
node --input-type=module -e "import * as m from 'some-ui-lib'; console.log(Object.keys(m))"
#    The two lists can differ (CJS vs ESM build). Compare them with your import.

And a decision tree for what you saw in step 2:

what did console.log show?
│
├─ undefined
│    ├─ import came from my own file ──► named vs default mismatch, or file never exports it (cause 1)
│    ├─ import came from a package ───► renamed/removed export after an upgrade (cause 2)
│    ├─ value is read at module top level, files import each other ──► cycle (cause 3)
│    └─ it's Foo.Bar ─────────────────► Bar never attached to Foo (cause 4)
│
├─ { }  (empty)  ───────────────────► half-loaded CJS module, i.e. cycle (cause 3)
├─ [Module] / { Card: ... } ────────► wrapper object: namespace or CJS interop (cause 5)
├─ false / null / true ─────────────► your own conditional (cause 6)
└─ <section /> ─────────────────────► file exports an element, not a function (cause 6)

The ranked causes. The ordering is from what I see most in code review and bug reports, not from a measured dataset, so weigh it accordingly.

RankCauseHow often it's the culpritThe tellFix
1Default import of a named export (or the reverse)Most commonundefined + hint; bundler is webpack-like or file is CJSOption A
2Package renamed/removed the export in an upgradeCommon after dependency bumpsworked on the old lockfile; Object.keys shows no such nameOption B
3Circular import read at module loadLess common, hardest to seeundefined or {}; value is stored in an array/object/HOC at top levelOption C
4Compound component member never attached (Tabs.Panel)Occasionalgot: undefined, the JSX has a dotOption D
5Whole module imported (import * as, CJS interop)Occasionalgot: object, no hintOption A
6Your own value: cond && 'h2', null, JSX literal exportedOccasionalboolean, null, or <section />Option E
7lazy() of a module without a defaultOccasionaldifferent message, code #306Option F
8memo/forwardRef wrapping an undefinedRare18.x: propTypes TypeErrorOption A or C, then re-wrap

The one that bit me while building this was rank 3, partly because of the dead end above. The tell was that the value existed in one place and not another: console.log(Home) inside Home.cjs printed a function, and console.log(Home) inside routes.cjs, at the top level, printed undefined. Same name, same file loaded from two places, different answers. Whenever a binding is defined in the file that exports it but missing in a file that imports it, stop suspecting the import syntax and look for a loop.

A look-alike, so you don't chase it: <card /> with a lowercase first letter does not throw this. React 19.3.1 renders it as an unknown DOM tag and logs The tag <card> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter. In my run the root rendered empty with no thrown error.

Fix the import, not the render

Where the fix belongs matters. Don't patch the element. Fix the module boundary.

Option A: match the import to the export (causes 1, 5, 8). One sentence on why: a default import binds to whatever the module says is default, and a module with only named exports has no default.

- import Card from "./Card";
+ import { Card } from "./Card";

For a namespace import, use the member, not the namespace:

- import * as Card from "./Card";
- <Card />
+ import { Card } from "./Card";
+ <Card />

Option B: follow the package's rename (cause 2). Open the package's changelog for the version you bumped; don't guess. The Object.keys command from step 4 gives you the new name. If you can't move yet, pin the old version in package.json and deal with it in its own PR.

Option C: break the cycle (cause 3). The reliable fix is moving the shared piece into a third file that both import, so neither imports the other. The quick fix is reading the value lazily so it's resolved at render time, not at load time:

- export const routes = [{ path: "/", component: Home }];
+ export const getRoutes = () => [{ path: "/", component: Home }];

The quick fix works but leaves the loop in place. I'd ship the third file.

Option D: attach the member (cause 4).

  function Tabs({ children }) { return <div>{children}</div>; }
  Tabs.List = function List() { return <ul />; };
+ Tabs.Panel = function Panel() { return <section />; };

Option E: stop computing component types from falsy values (cause 6).

- const H = heading && "h2";
- return <H>title</H>;
+ const H = heading ? "h2" : "div";
+ return <H>title</H>;

And if a file exports <section>hi</section>, export a function instead: export default function Section() { return <section>hi</section>; }.

Option F: give lazy a default (cause 7).

- const L = lazy(() => import("./Card"));
+ const L = lazy(() => import("./Card").then((m) => ({ default: m.Card })));

If this only fails after a deploy, and only for some users, it isn't this. That's the stale-chunk story in Failed to fetch dynamically imported module.

The silencers to refuse. Wrapping the render in Card ? <Card /> : null hides the crash and ships a blank region. An error boundary around everything is a fine production safety net, but it's not a fix, and it will happily swallow this error into a generic fallback. Both leave the missing export where it was.

Make a missing export fail the build

The design goal is to make "the import is undefined" something the toolchain says out loud before a user does. What I saw, with versions:

# esbuild 0.28.2
✘ [ERROR] No matching export in "src/Card.jsx" for import "default"

# vite 8.3.5 (vite build, exit status 1)
[MISSING_EXPORT] "default" is not exported by "src/Card.jsx".

# webpack 5.112.2 (exit status 0; the bundle ran and logged `undefined`)
WARNING in ./src/index.jsx 2:19-23
export 'default' (imported as 'Card') was not found in './Card.jsx' (possible exports: Card)

# tsc 5.9.3, strict, moduleResolution: bundler
App.tsx(1,8): error TS2613: Module '"./Card"' has no default export. Did you mean to use 'import { Card } from "./Card"' instead?
App.tsx(2,10): error TS2724: '"./Card"' has no exported member named 'Cardd'. Did you mean 'Card'?

Webpack is the outlier: the same mistake that stops Vite cold produces a warning there. If you use webpack, turn on module.parser.javascript.strictExportPresence: true. I ran the same file with it on webpack 5.112.2 and the warning became ERROR in ./src/index.jsx 2:19-23, compiled with 1 error, exit status 1.

A sample of how I'd write the compound-component case so the typo can't reach runtime. Type-checked with typescript@5.9.3 (target ES2022, strict):

// Tabs.tsx
import type { ReactNode } from "react";

function TabsRoot({ children }: { children: ReactNode }) {
  return <div role="tablist">{children}</div>;
}
function List() {
  return <ul />;
}
function Panel({ id }: { id: string }) {
  return <section id={id} />;
}

// Object.assign gives the compound component a type that knows its members.
export const Tabs = Object.assign(TabsRoot, { List, Panel });
// Use.tsx
import { Tabs } from "./Tabs";

export const Ok = () => (
  <Tabs>
    <Tabs.List />
    <Tabs.Panel id="a" />
  </Tabs>
);
export const Typo = () => <Tabs.Pannel id="a" />;
// tsc: error TS2551: Property 'Pannel' does not exist on type '...'. Did you mean 'Panel'?

The rest of the design is dull and effective. Prefer named exports for components, so a wrong import name is an error everywhere, not a silent default. Keep barrel files (index.ts that re-export a folder) out of the middle of import graphs, because they're where most cycles start. And export components, never elements.

Lint, types, and the one setting that matters

  • Types: tsc --noEmit with strict. It caught both the default/named mismatch (TS2613) and the misspelled member (TS2724 / TS2551) above, and it runs in seconds.
  • Lint: import/named, import/default and import/no-cycle from eslint-plugin-import target exactly causes 1, 2 and 3. I didn't run them for this article, so confirm the rule names against the plugin major you have installed.
  • CI: build with the same bundler production uses, and fail on warnings (for webpack, strictExportPresence above). A dev server on Vite won't show you what a webpack production build does.
  • Tests: render each route once in a smoke test. A route that reaches undefined fails loudly there, not in the user's browser. Playwright can also fail a run on any pageerror event.
  • Production signal: alert on Minified React error #130 and #306 in your error tracker. They're rare enough that one occurrence after a deploy is a regression. With the second args[] empty, rely on release version and route to localise it.
  • Version safety: the message and the undefined / empty-object hint behave the same on every line I ran, 18.3.1 through 19.3.1. Two things to re-check on the next upgrade: whether memo(undefined) still gives the friendly error (it didn't on 18), and whether the decoder host your logs link to still resolves.

Team convention worth writing down: components are named exports; default exports are for route files that a framework requires.

What transfers

  • got: is a diagnosis. undefined with a hint is a missing binding, object with no hint is a wrapper, object with a hint is empty, boolean is your own conditional.
  • A value that exists in its home file and is missing at an importer is a cycle until proven otherwise.
  • The toolchain decides whether this is a build error or a runtime error. Make the build the loud one.
  • Before trusting any multi-version test, print which version actually loaded.

Questions that lead here

Why does "Element type is invalid" say "got: object"? Because the thing in the tag position is an object React doesn't recognise as a component. With keys and no hint, it's usually a whole module (import * as X) or a CommonJS exports object. With the "you likely forgot to export" sentence, the object is empty. React 18.3.1 and 19.x behave the same here.

How do I fix "You likely forgot to export your component from the file it's defined in"? Check whether you imported with braces or without, and whether the file uses export default or export const. They must match. If the import comes from a package, run Object.keys on its require and import forms and compare with what you wrote.

Why did this only appear after I built for production? Usually because your dev server and your production bundler treat a missing export differently. In my runs esbuild and Vite stopped the build, and webpack only warned and shipped undefined. The other reason is a circular import whose order changes once code is chunked and minified.

What does Minified React error #130 mean? It is this error. The first args[] in the link is the got: value (undefined, object, boolean…), the second is the owner hint and is empty in production. The lazy version is #306.

For the neighbouring cases where a perfectly valid value is the problem, see Objects are not valid as a React child, and for the duplicate-React family that also shows up as a "should be fine" bug in monorepos, Invalid hook call when your hooks are fine. If your trigger was a property read on a value that never arrived, that's Cannot read properties of undefined.

Reactreactjses-modulesimportsdebugging

BKG

Engineer and writer behind CODELZ. I read the stack trace, reproduce the bug, and explain why it happens, not just how to silence it. Plus honest reviews of the books and tools worth your time.

Comments