require is not defined in React: Three Different Errors, One Message
The same ReferenceError means three unrelated things. Measured in Chromium and Node 22, plus the two ways this repo gets __dirname with zero require calls.
require is not defined means the code calling require() is running somewhere require does not exist — a browser, or a Node file Node decided is an ES module. React does not supply require; a CommonJS loader does. The fix is never to define it: use import, or move the call to a file that is genuinely CommonJS.
The message is identical in three unrelated situations, which is why the usual advice ("switch to import") works for some people and does nothing for others. This post separates them by the one thing that tells them apart — where the stack trace points — and then shows what this storefront does instead, since it has zero require( calls in src/ and scripts/ combined and still needs the two things people reach for require to get.
Which of the three do you have?
Read the first line of the error, not the message.
| What the error says | Where it ran | Cause |
|---|---|---|
Uncaught ReferenceError: require is not defined, in the browser console, with a .js/.jsx source | Browser | No bundler ran. require was never transformed into anything the browser has. |
ReferenceError: require is not defined in ES module scope, you can use import instead, from node | Node, ESM | The file is .mjs, or package.json has "type": "module". |
require is not defined from a file you wrote in TypeScript | Node, ESM | tsc/tsx emitted ESM ("module": "esnext"), so the require you typed was left alone, not downlevelled. |
The second message is the generous one: Node names the scope and the fix. The browser's is a bare ReferenceError with no hint that a build step is missing.
Case 1: the browser, where the whole script block dies
Measured while writing this, in headless Chromium against a page whose only script calls require("react"):
[pageerror] ReferenceError: require is not defined
root text: ""
The second line is the part worth keeping. #root was not partially rendered — it was empty. A ReferenceError at the top of a classic <script> aborts the rest of that block, so the document.getElementById("root").textContent = "mounted" two lines below it never ran. People read the blank page as "React failed to mount" and start debugging React. React was never reached.
This happens whenever React is loaded from a CDN <script> tag and the component files are then split up as if a bundler were present. There is no module system on that page. Two honest ways out:
- Keep it bundler-less and use the global. With the UMD build loaded, the component is
React.createElement, notrequire("react"). No imports at all. - Add a build step. Vite, Next.js or
create-react-appall makeimportwork because something rewrites it before the browser sees it. This is the answer for anything beyond a single file.
What does not work is defining window.require yourself. You can make the error go away and you will have a function that cannot resolve "react", because resolution is the part the bundler was doing.
The same error inside a React component on a modern setup
A bundled app can still produce it, in one specific way: a require() reached at runtime in code shipped to the browser. In the Next.js App Router that is a "use client" file, or a server file a client component pulled in. The bundler will often rewrite require() at build time, so the symptom appears only for the paths it could not resolve statically — a template literal, a variable, anything computed:
// Fails in the browser: nothing to rewrite at build time.
const mod = require(`./locales/${locale}.js`);
Dynamic import() is the direct replacement, and it returns a promise:
const mod = await import(`./locales/${locale}.js`);
Case 2: Node, where the file extension decides
Node picks a module system per file, from the extension and from package.json:
| File | Module system | require | __dirname | import.meta.url |
|---|---|---|---|---|
.cjs | CommonJS | Yes | Yes | No |
.mjs | ESM | No | No | Yes |
.js, no "type" in package.json | CommonJS | Yes | Yes | No |
.js, "type": "module" | ESM | No | No | Yes |
Verified on Node v22.22.0 with one file, copied byte-for-byte to two extensions:
$ cat demo.mjs
const data = require("node:fs");
$ node demo.mjs
ReferenceError: require is not defined in ES module scope, you can use import instead
at file:///tmp/esmdemo/demo.mjs:1:14
$ cp demo.mjs demo.cjs && node demo.cjs # same bytes
$ echo $?
0
The code is not wrong. The extension is. That is the whole of case 2, and it explains the most common way people hit it: a project adds "type": "module" to package.json for one reason, and every pre-existing .js file with a require in it breaks at once.
What this repo does instead of require
This storefront is Next.js 16 with 220 TypeScript files under src/, plus 16 .mjs and 13 .ts scripts under scripts/. package.json has no "type" field at all, so .js here would be CommonJS — and the count of require( calls across src/ and scripts/ is 0. Every script that needs to be ESM says so with its extension rather than flipping the whole package.
The interesting part is __dirname, which disappears alongside require and is the thing most scripts actually wanted. Both halves of this repo need it and they solve it differently, correctly, in the same tree.
scripts/seo/lib/corpus.mjs is ESM by extension, so it reconstructs the directory from the module's own URL:
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const HERE = dirname(fileURLToPath(import.meta.url));
export const ROOT = join(HERE, "..", "..", "..");
vitest.config.ts uses the bare global, four directories away:
import path from "node:path";
export default defineConfig({
resolve: {
alias: { "@": path.resolve(__dirname, "src") },
},
});
That is not an inconsistency. Vitest loads its config through Vite, which transpiles the file and supplies __dirname; nothing is asking Node to interpret those bytes directly. The rule that covers both: __dirname is available if something is compiling the file, and absent if Node is reading it as ESM. When in doubt, import.meta.url works in every ESM context and never lies about which file it is in.
The adjacent trap: a module reference that compiles and still vanishes
Worth knowing, because the symptom is the opposite of a ReferenceError — nothing errors at all. This repo configures MDX with its plugins named as strings:
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"],
rehypePlugins: ["rehype-slug"],
},
});
Importing remarkGfm and passing the function instead type-checks, builds, and silently does nothing — Turbopack runs the MDX pipeline in Rust and cannot receive a JavaScript function reference. The failure showed up as every | a | b | table in every comparison post rendering as a paragraph of literal pipes.
The lesson generalises past MDX: when a module reference crosses out of JavaScript — into a Rust pipeline, a worker, a serialised config — it has to cross as a name, not as a value. require failing loudly in the browser and a plugin function failing silently in a config are the same mistake about where code is actually running.
Mistakes and how they show up
| Symptom | Real cause | Fix |
|---|---|---|
Blank page, require is not defined in console | No bundler; CDN React with split component files | Use the React global, or add Vite/Next.js |
| Error names a file you only ever run in Node | File is .mjs, or "type": "module" is set | Rename to .cjs, or convert the file to import |
Worked before you added "type": "module" | Every .js in the package is now ESM | Rename the CommonJS holdouts to .cjs |
__dirname is not defined right after fixing require | Same cause — both are CommonJS-only globals | dirname(fileURLToPath(import.meta.url)) |
Only one require in the file fails | The path is computed, so the bundler could not rewrite it | await import(...) |
require works in your test run but not in node | A transpiler (Vite, tsx, Jest) is supplying it | Don't rely on it outside that runner |
Defining window.require "fixed" it, then nothing resolved | require needs a resolver, not a function | Remove it; add a real build step |
Frequently asked questions
Why does React specifically throw this?
It does not. React never defines require. The error comes from the environment, and React is simply the most common thing people were trying to load when it appeared — the stack trace mentions React because require("react") is the line that ran.
Can I use require in a Next.js project?
In server-side code that the bundler compiles, usually yes, and next.config.js accepts it. In a "use client" component, treat it as unavailable: use import at the top of the file, or await import() when the path is dynamic. This repo uses neither — 0 calls in 220 files — which is the simplest position to defend.
How do I use require and import in the same project?
Per file, by extension: .cjs for CommonJS, .mjs for ESM. Both can live in one package. What you cannot do is mix them in a single file — "type": "module" is a package-wide switch, and a mixed file is the error in case 2.
Is import always the right replacement?
For a static dependency, yes. For a conditional or computed path, require() was synchronous and import() is not, so the call site has to become async. That change is usually small and is the only real migration cost.
Landing pages with the module system already decided
If the current task is shipping a page rather than arbitrating module systems, ASoc Pip, ASoc Press and ASoc Quest are landing sets that build clean on the toolchain described above — ESM throughout, no require, no bundler configuration to repair.
Browse the sets: Next.js landing page templates and Tailwind landing page templates.
