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
- Pass watch: true to createConfigLoader if you need change events
- Otherwise do not register onChange — call loader.getConfig() on demand instead
- 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
- Track the watch flag you passed and branch onChange registration on it
- Use loadConfig() for one-shot reads — it never exposes onChange
- Treat onChange availability as part of the loader contract, not a universal method
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
- React Router presets must have a `name` property defined.
- Could not find package.json in ${rootDirectory} or any of it
- React Router Vite plugin not found in Vite config
- Custom Vite manifest paths are not supported
- The React Router Vite plugin requires the use of a Vite conf
AI-assisted analysis of remix-run/react-router@6beaca3952 (2026-08-18).
Data as JSON: /api/errors/6a87f8bc049ccf89.
Report an issue: GitHub.