Skip to main content
Your application often needs to know the moment a config value changes — whether the change originated inside your own code or was made by an external tool editing the file directly on disk. tauri-plugin-configurate exposes two dedicated methods for this: onChange listens for operations your app performs, and watchExternal hands you a live feed of changes made by other processes. Both return cleanup functions so you can remove listeners precisely when they are no longer needed.

onChange(callback)

onChange registers a listener that fires whenever your application performs any write operation on this config target. The operations it covers are: create, save, patch, delete, reset, and import. It does not fire for changes made by external processes — use watchExternal for that. The method is async and returns a Promise<() => void>. You must await it to get the unlisten function; the unlisten function itself is synchronous. Call it when you no longer need the listener.
const unlisten = await config.onChange((event) => {
  console.log(`Config ${event.operation} on ${event.fileName}`);
});

// Stop listening when done
unlisten();
onChange is useful for keeping derived UI state in sync — for example, marking a settings form as “saved” after a save or patch fires.

watchExternal(callback)

watchExternal registers an OS-level file-system watcher on the config file and calls your callback whenever an external process modifies it. The callback receives a ConfigChangeEvent whose operation field is always "external_change". The method returns an async stop function. You must await it to cleanly unregister both the JS event listener and the Rust-side file watcher.
const stopWatching = await config.watchExternal((event) => {
  console.log(`External change detected: ${event.operation}`);
});

// Stop watching when done
await stopWatching();
watchExternal is supported for file-based providers only (JSON, YAML, TOML, Binary). Calling it on a SQLite-backed config throws an error immediately. Always use watchExternal in a try/catch block if the provider type is not known at compile time.

ConfigChangeEvent

Both onChange and watchExternal pass a ConfigChangeEvent to your callback:
interface ConfigChangeEvent {
  fileName: string;   // Config file name (e.g. "app.json")
  operation: string;  // "create" | "save" | "patch" | "delete" | "reset" | "import" | "external_change"
  targetId: string;   // Unique identifier for this Configurate instance
}
The targetId is derived from the combination of fileName, baseDir, provider kind, and path options. It lets you safely share a single global event bus and route events to the right handler when you have multiple Configurate instances.

Typical pattern: auto-reload on external change

A common use case is reloading the in-memory config automatically when an external editor modifies the file — for example, a developer tweaking settings during development, or a companion CLI tool writing a config update.
const stopWatching = await config.watchExternal(async () => {
  const { data } = await config.load().run();
  applySettings(data);
});
Because the callback is invoked with the raw ConfigChangeEvent, you can also inspect event.fileName or event.targetId before deciding whether to reload.

Cleaning up listeners

1

Store the cleanup function

Assign the return value of onChange or watchExternal to a variable in the scope that owns the listener’s lifetime.
let unlisten: (() => void) | null = null;
let stopWatching: (() => Promise<void>) | null = null;

unlisten = await config.onChange(handleChange);
stopWatching = await config.watchExternal(handleExternal);
2

Call cleanup on unmount or teardown

In a React component, call cleanup inside the useEffect return. In a Svelte component, use onDestroy. In plain TypeScript, call them whenever the owning object is disposed.
// React example
useEffect(() => {
  let unlisten: (() => void) | undefined;

  config.onChange((event) => {
    setLastOp(event.operation);
  }).then((fn) => { unlisten = fn; });

  return () => { unlisten?.(); };
}, []);
3

Await the watchExternal stop function

Unlike onChange, the stop function returned by watchExternal is async. Always await it so the Rust-side watcher is properly unregistered before the component or window closes.
// Svelte example
import { onDestroy } from "svelte";

let stopWatching: (() => Promise<void>) | undefined;

config.watchExternal(handleExternal).then((fn) => {
  stopWatching = fn;
});

onDestroy(async () => {
  await stopWatching?.();
});
Always clean up both onChange and watchExternal listeners when their owning component unmounts or their owning object is destroyed. Uncleaned listeners hold references to the Tauri event system and can cause memory leaks or ghost callbacks in long-running applications.