Skip to content

watchEffect

Runs a function immediately while automatically tracking its reactive dependencies, and re-runs it whenever those dependencies change.

Signature

ts
function watchEffect<T>(
  effect: (onCleanup?: (cleanupFn: () => void) => void) => T,
  options?: WatchEffectOptions
): WatchEffectStopHandle<T>;

Parameters

  • effect: A function that will be run immediately and re-run when its dependencies change. This function receives an optional onCleanup function as its first argument that allows you to register cleanup callbacks.
    • onCleanup?: Function to register cleanup callbacks that will be called before the effect re-runs or when the effect is stopped
  • options: Optional settings object with the following properties:
    • lazy?: If true, the effect does not run immediately upon creation
    • scheduler?: Custom function to control how/when the effect runs after a trigger
    • onTrack?: Debug callback for when a reactive property is tracked
    • onTrigger?: Debug callback for when the effect is triggered

Return Value

Returns a stop handle function that can be called to stop the watcher. The stop handle also exposes the internal effect instance for advanced use cases:

ts
interface WatchEffectStopHandle<T = any> {
  (): void; // Stop function - call to stop watching
  effect: TrackedEffect<T>; // Internal effect instance
}

When the stop function is called, any registered cleanup functions will be executed before the effect is permanently stopped.

Examples

Basic Usage

ts
import { reactive, watchEffect } from "@yiin/reactive-proxy-state";

const state = reactive({ count: 0 });

const stop = watchEffect(() => {
  console.log(`Count is: ${state.count}`);
});
// Output: Count is: 0

state.count++;
// Output: Count is: 1

// Stop watching
stop();

state.count++;
// No output - the effect is no longer running

Automatic Dependency Tracking

One of the key features of watchEffect is that dependencies are automatically tracked:

ts
import { reactive, watchEffect } from "@yiin/reactive-proxy-state";

const user = reactive({
  firstName: "John",
  lastName: "Doe",
  age: 30,
});

watchEffect(() => {
  // Only properties accessed inside this function will be tracked
  console.log(`Name: ${user.firstName} ${user.lastName}`);
});
// Output: Name: John Doe

// This triggers the effect because firstName was accessed in the effect
user.firstName = "Jane";
// Output: Name: Jane Doe

// This does NOT trigger the effect because age wasn't accessed in the effect
user.age = 31;
// No output

Cleanup Functions with onCleanup

The effect function receives an onCleanup function as its first argument. You can call this to register cleanup callbacks that will be executed:

  • Before the effect re-runs (when dependencies change)
  • When the effect is stopped (via the stop handle)

This is essential for cleaning up side effects like timers, event listeners, subscriptions, etc.

ts
import { ref, watchEffect } from "@yiin/reactive-proxy-state";

const id = ref(0);

const stop = watchEffect((onCleanup) => {
  const currentId = id.value;
  console.log(`Effect running for id: ${currentId}`);

  // Set up a side effect
  const timer = setTimeout(() => {
    console.log(`Timer ${currentId} fired`);
  }, 1000);

  // Register cleanup function - will be called before re-execution or on stop
  onCleanup(() => {
    clearTimeout(timer);
    console.log(`Timer ${currentId} cleared`);
  });
});

// After 500ms, change the id which will trigger cleanup and re-run
setTimeout(() => {
  id.value++; // Triggers cleanup, then re-runs effect
}, 500);

// Later, stop the effect (triggers final cleanup)
setTimeout(() => {
  stop(); // Calls cleanup one final time
}, 2000);

// Output:
// Effect running for id: 0
// (after 500ms)
// Timer 0 cleared
// Effect running for id: 1
// (after 1500ms more)
// Timer 1 cleared

Multiple Cleanup Functions

You can register multiple cleanup functions - they will all be called in the order they were registered:

ts
const state = ref(0);

watchEffect((onCleanup) => {
  console.log(`Setting up for state: ${state.value}`);

  // Multiple cleanup registrations
  onCleanup(() => console.log("Cleanup 1"));
  onCleanup(() => console.log("Cleanup 2"));
  onCleanup(() => console.log("Cleanup 3"));
});

state.value++;
// Output:
// Setting up for state: 0
// (when state changes)
// Cleanup 1
// Cleanup 2
// Cleanup 3
// Setting up for state: 1

Error Handling in Cleanup

If a cleanup function throws an error, it won't break the effect or prevent other cleanup functions from running:

ts
watchEffect((onCleanup) => {
  onCleanup(() => {
    throw new Error("Cleanup error");
  });
  onCleanup(() => {
    console.log("This cleanup still runs");
  });

  return state.value;
});

// Errors are logged to console but don't break the effect

Debugging with onTrack and onTrigger

The onTrack and onTrigger options can be used to debug when dependencies are tracked and when the effect is triggered:

ts
import { reactive, watchEffect } from "@yiin/reactive-proxy-state";

const state = reactive({ count: 0 });

watchEffect(
  () => {
    console.log(`Count is: ${state.count}`);
  },
  {
    onTrack(event) {
      console.log("Property tracked:", event);
    },
    onTrigger(event) {
      console.log("Effect triggered:", event);
    },
  }
);

state.count++;

Stopping the Watcher

The returned stop handle can be used to stop the watcher when you no longer need it:

ts
import { reactive, watchEffect } from "@yiin/reactive-proxy-state";

const state = reactive({ count: 0 });

const stop = watchEffect(() => {
  console.log(`Count is: ${state.count}`);
});
// Output: Count is: 0

// Later, when you want to stop watching
stop();

// This will no longer trigger the effect
state.count++;

Self-stopping Watchers

A watcher can also stop itself during execution:

ts
import { ref, watchEffect } from "@yiin/reactive-proxy-state";

const count = ref(0);

const stop = watchEffect((onCleanup) => {
  console.log(`Count is: ${count.value}`);

  // Stop watching when count reaches 3
  if (count.value >= 3) {
    stop();
  }
});
// Output: Count is: 0

count.value++;
// Output: Count is: 1

count.value++;
// Output: Count is: 2

count.value++;
// Output: Count is: 3
// Watcher stops itself

count.value++;
// No output - the effect is no longer running

Comparison with watch

Unlike watch, which focuses on specific sources and gives you access to old and new values:

  • watchEffect automatically tracks dependencies
  • watchEffect can track multiple reactive sources at once
  • watchEffect doesn't give you access to previous values
  • watchEffect runs immediately by default

If you need to compare old and new values, or have more control over when the callback is executed, use watch instead.

  • watch – watch specific sources with old/new comparison
  • computed – cached derived reactive values that internally use watchEffect
  • ref – reactive primitive wrapper commonly used inside watchEffect
  • reactive – create reactive objects that supply dependencies