Initializing Enclave...

How to Fix Webpack 'Configuration Object Does Not Match API Schema' After Adding react-refresh

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: react-refresh was wired into the Babel config (or webpack plugins array) incorrectly — either instantiated without new, missing its paired ReactRefreshWebpackPlugin, or not guarded behind a isDevelopment conditional — causing webpack's internal AJV schema validator to reject the entire config object at startup.
  • How to fix it: Install @pmmmwh/react-refresh-webpack-plugin, instantiate it with new, and wrap both the Babel plugin and the webpack plugin inside a isDevelopment guard so they never run in production builds.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your webpack.config.js and get a corrected diff without sending your config to any external server.

The Incident (What Does the Error Mean?)

You ran webpack serve or webpack build and got slapped with this wall of red:

Invalid configuration object. Webpack has been initialized using a
configuration object that does not match the API schema.
 - configuration.plugins[2] should be one of these:
   object { apply, … } | function
   -> Plugin Instance (with an apply function) or a class (which is applied on instantiation)

or a variant like:

TypeError: Cannot read properties of undefined (reading 'apply')
    at webpack (node_modules/webpack/lib/webpack.js:...)

or, after adding the Babel plugin entry:

Error: [react-refresh/babel] This plugin should not be used in a production environment.

Immediate consequence: webpack refuses to initialize. Zero bundles are emitted. Your dev server never starts. CI pipeline fails on the very first step. Every developer on the team is blocked.

The schema validator webpack uses (AJV under the hood) is strict. It expects every entry in plugins[] to be an object with an apply method — i.e., a properly instantiated plugin class. A raw class reference, undefined, or a plain object without apply causes an immediate hard rejection before a single module is resolved.


The Attack Vector / Blast Radius

This is a build-system availability failure, not a runtime security exploit — but the blast radius is wider than it looks:

  1. Total CI/CD pipeline blockage. Every PR build fails. Merge queues back up. If your team ships multiple times per day, this is a direct velocity tax measured in engineer-hours per hour of downtime.

  2. Production config contamination. The most dangerous variant: react-refresh/babel gets accidentally left in the Babel config for production builds. React Refresh injects a runtime module (react-refresh/runtime) into every component file. That module is ~40 KB unminified, contains HMR socket logic, and throws an explicit error in production because window.__reactRefreshInjected is never set by the absent webpack plugin. Result: your production bundle crashes on load for every user.

  3. Dependency version skew. react-refresh (the Babel transform) and @pmmmwh/react-refresh-webpack-plugin (the webpack plugin) must be version-compatible. Mismatched versions silently produce a plugin object that fails schema validation only at runtime, not at npm install time — so your lockfile looks clean but your build is broken.

  4. Monorepo hoisting collisions. In yarn/pnpm workspaces, react-refresh can be hoisted to the root while the webpack plugin resolves a different copy from a nested node_modules. The schema error is the symptom; the root cause is two incompatible instances.


How to Fix It (The Solution)

Step 0 — Install the missing paired package

npm install --save-dev @pmmmwh/react-refresh-webpack-plugin react-refresh
# or
yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh

If you only installed react-refresh and added the Babel plugin, you have half the system. The webpack plugin is mandatory.


Basic Fix — webpack.config.js

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
+
+const isDevelopment = process.env.NODE_ENV !== 'production';
 
 module.exports = {
   mode: 'development',
   entry: './src/index.js',
   module: {
     rules: [
       {
         test: /\.[jt]sx?$/,
         exclude: /node_modules/,
         use: [
           {
             loader: 'babel-loader',
             options: {
               plugins: [
-                'react-refresh/babel',
+                isDevelopment && 'react-refresh/babel',
-              ],
+              ].filter(Boolean),
             },
           },
         ],
       },
     ],
   },
   plugins: [
     new HtmlWebpackPlugin({ template: './public/index.html' }),
-    ReactRefreshWebpackPlugin,
+    isDevelopment && new ReactRefreshWebpackPlugin(),
-  ],
+  ].filter(Boolean),
   devServer: {
-    hot: false,
+    hot: true,
   },
 };

Three rules to memorize:

  1. Always new ReactRefreshWebpackPlugin() — never pass the class reference bare.
  2. Always guard both the Babel plugin and the webpack plugin behind isDevelopment.
  3. Always .filter(Boolean) the plugins array so false entries don't leak through.

Enterprise Best Practice — Split Config Files

For teams running webpack in CI with multiple environments, inline process.env.NODE_ENV ternaries become unmaintainable. Use webpack-merge:

 // webpack.common.js  — no react-refresh here at all
 module.exports = {
   entry: './src/index.js',
   module: { rules: [ /* base rules, no babel react-refresh plugin */ ] },
   plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }) ],
 };
 // webpack.dev.js
+const { merge } = require('webpack-merge');
+const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
+const common = require('./webpack.common.js');
+
+module.exports = merge(common, {
+  mode: 'development',
+  devServer: { hot: true },
+  module: {
+    rules: [
+      {
+        test: /\.[jt]sx?$/,
+        exclude: /node_modules/,
+        use: [{ loader: 'babel-loader', options: { plugins: ['react-refresh/babel'] } }],
+      },
+    ],
+  },
+  plugins: [new ReactRefreshWebpackPlugin()],
+});
 // webpack.prod.js
+const { merge } = require('webpack-merge');
+const common = require('./webpack.common.js');
+
+module.exports = merge(common, {
+  mode: 'production',
+  // react-refresh is structurally absent — no conditional needed
+});

This eliminates the entire class of "forgot the isDevelopment guard" bugs because react-refresh is architecturally isolated to the dev config file.


💡 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. Validate webpack config as a pre-commit hook

Add a package.json script that dry-runs webpack config validation:

{
  "scripts": {
    "validate:webpack": "webpack --config webpack.dev.js --dry-run 2>&1 | grep -i 'error\|schema' && exit 1 || exit 0"
  }
}

Hook it with husky:

npx husky add .husky/pre-commit "npm run validate:webpack"

2. Enforce environment separation with ESLint

Use eslint-plugin-node or a custom rule to flag require('react-refresh') appearing in any file not matching *.dev.* or webpack.dev.js:

{
  "rules": {
    "no-restricted-modules": ["error", {
      "paths": [{
        "name": "react-refresh/babel",
        "message": "react-refresh/babel must only appear in webpack.dev.js or a dev-only Babel config."
      }]
    }]
  }
}

3. Pin compatible versions with Renovate/Dependabot grouping

In renovate.json, group react-refresh and @pmmmwh/react-refresh-webpack-plugin so they are always updated together:

{
  "packageRules": [
    {
      "matchPackageNames": ["react-refresh", "@pmmmwh/react-refresh-webpack-plugin"],
      "groupName": "react-refresh bundle",
      "automerge": false
    }
  ]
}

4. Schema validation in CI with webpack-cli --json

# .github/workflows/build.yml
- name: Validate webpack schema
  run: |
    npx webpack --config webpack.dev.js --json > /dev/null
    npx webpack --config webpack.prod.js --json > /dev/null
  env:
    NODE_ENV: production

This catches schema errors on every PR before a single test runs, making the feedback loop seconds instead of minutes.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →