remix-run/react-router · error · Error

onChange is not supported when watch mode is disabled

Error message

onChange is not supported when watch mode is disabled

What it means

createConfigLoader() only sets up a chokidar file watcher when constructed with watch: true. Calling onChange() on a loader built with watch: false throws immediately because there is no watcher to subscribe handlers to — the guard sits at the top of onChange before any handler registration.

Source

Thrown at packages/react-router-dev/config/config.ts:870

    validateConfig,
  });

  if (!initialConfigResult.ok) {
    throw new Error(initialConfigResult.error);
  }

  appDirectory = Path.normalize(initialConfigResult.value.appDirectory);

  let currentConfig = initialConfigResult.value;

  let fsWatcher: FSWatcher | undefined;
  let changeHandlers: ChangeHandler[] = [];

  return {
    getConfig,
    onChange: (handler: ChangeHandler) => {
      if (!watch) {
        throw new Error(
          "onChange is not supported when watch mode is disabled",
        );
      }

      changeHandlers.push(handler);

      if (!fsWatcher) {
        fsWatcher = chokidar.watch([root, appDirectory], {
          ignoreInitial: true,
          ignored: (path) => isIgnoredByWatcher(path, { root, appDirectory }),
        });

        fsWatcher.on("error", (error: unknown) => {
          let message = error instanceof Error ? error.message : String(error);
          console.warn(colors.yellow(`File watcher error: ${message}`));
        });

        fsWatcher.on("all", async (...args) => {

View on GitHub (pinned to 6beaca3952)

Solutions

  1. Pass watch: true to createConfigLoader if you need change events
  2. Otherwise do not register onChange — call loader.getConfig() on demand instead
  3. For one-shot reads use loadConfig(), which never exposes onChange

Example fix

// before
const loader = await createConfigLoader({ rootDirectory, mode, watch: false });
loader.onChange(handler); // throws

// after
const loader = await createConfigLoader({ rootDirectory, mode, watch: true });
const unsubscribe = loader.onChange(handler);
Defensive patterns

Strategy: validation

Validate before calling

import { createConfigLoader } from "@react-router/dev/config";

const watch = process.env.NODE_ENV === "development";
const loader = await createConfigLoader({ rootDirectory, mode, watch });

// Only subscribe when the loader was created with watch: true
const unsubscribe = watch ? loader.onChange(handler) : null;

Type guard

type WatchableConfigLoader = ReturnType<
  typeof createConfigLoader<{ watch: true }>
> & { onChange(handler: (evt: unknown) => void): () => void };

function supportsOnChange(
  loader: Awaited<ReturnType<typeof createConfigLoader>>,
): loader is WatchableConfigLoader {
  return "onChange" in loader;
}

Prevention

When it happens

Trigger: Programmatic use: const loader = await createConfigLoader({ ..., watch: false }); loader.onChange(handler). Common when code written against the dev-server path (watch: true) is reused for one-shot build/load paths, or loadConfig-adjacent utilities are handed a non-watch loader.

Common situations: Custom build scripts or plugins calling @react-router/dev internals; refactors where onChange registration moved outside the watch-enabled branch.

Related errors


AI-assisted analysis of remix-run/react-router@6beaca3952 (2026-08-18). Data as JSON: /api/errors/6a87f8bc049ccf89. Report an issue: GitHub.