Skip to main content
ASoc
Tutorial

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.

The ASoc Team9 min read

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 saysWhere it ranCause
Uncaught ReferenceError: require is not defined, in the browser console, with a .js/.jsx sourceBrowserNo 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 nodeNode, ESMThe file is .mjs, or package.json has "type": "module".
require is not defined from a file you wrote in TypeScriptNode, ESMtsc/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, not require("react"). No imports at all.
  • Add a build step. Vite, Next.js or create-react-app all make import work 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:

FileModule systemrequire__dirnameimport.meta.url
.cjsCommonJSYesYesNo
.mjsESMNoNoYes
.js, no "type" in package.jsonCommonJSYesYesNo
.js, "type": "module"ESMNoNoYes

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

SymptomReal causeFix
Blank page, require is not defined in consoleNo bundler; CDN React with split component filesUse the React global, or add Vite/Next.js
Error names a file you only ever run in NodeFile is .mjs, or "type": "module" is setRename to .cjs, or convert the file to import
Worked before you added "type": "module"Every .js in the package is now ESMRename the CommonJS holdouts to .cjs
__dirname is not defined right after fixing requireSame cause — both are CommonJS-only globalsdirname(fileURLToPath(import.meta.url))
Only one require in the file failsThe path is computed, so the bundler could not rewrite itawait import(...)
require works in your test run but not in nodeA transpiler (Vite, tsx, Jest) is supplying itDon't rely on it outside that runner
Defining window.require "fixed" it, then nothing resolvedrequire needs a resolver, not a functionRemove 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.

Keep reading

Tutorial9 min read

Robots.txt and Sitemap.xml in Next.js: One Declared Date, 420 Routes

The STOREFRONT_COPY_REVISED fix for a sitemap that told Google 111 pages changed on a date nothing did, plus the noindex-vs-disallow split this codebase actually uses.

Read more
Tutorial8 min read

shadcn/ui on React 19: Nothing Breaks, and That Is the Problem

Both the forwardRef and the ref-as-prop shape typecheck on React 19.2.4. Eight peer ranges measured today, and the cleanup copy-in leaves you owning.

Read more