Skip to content

Watchers & Effects ​

Watchers let you run side effects when reactive dependencies change. Flutter Compositions provides two APIs: watch() for explicit dependency tracking, and watchEffect() for automatic tracking.

Prefer computed over watch: If your goal is to derive a new value from reactive state, use computed() instead. computed is declarative, automatically cached, and only recalculates when its dependencies change. Reserve watch/watchEffect for side effects — operations that interact with the outside world (logging, navigation, API calls, analytics, local storage, etc.) rather than producing a value.

dart
// ❌ Using watch to derive a value
final fullName = ref('');
watch(
  () => (firstName.value, lastName.value),
  (names, _) => fullName.value = '${names.$1} ${names.$2}',
);

// ✅ Using computed to derive a value
final fullName = computed(() => '${firstName.value} ${lastName.value}');

watch() ​

watch() explicitly specifies what to observe and gives you access to both old and new values:

dart
final count = ref(0);

watch(
  () => count.value,        // Getter: what to watch
  (newValue, oldValue) {    // Callback: what to do
    print('Count changed: $oldValue → $newValue');
  },
);

count.value = 1;  // Prints: "Count changed: 0 → 1"

Watching Multiple Sources ​

Combine multiple refs in the getter to react when any of them change:

dart
watch(
  () => (firstName.value, lastName.value),
  (newNames, oldNames) {
    print('Name changed from ${oldNames.$1} ${oldNames.$2} to ${newNames.$1} ${newNames.$2}');
  },
);

Watching Value Transitions ​

Use the previous parameter to compare old and new values:

dart
watch(() => cart.total.value, (total, previous) {
  if (total > previous) {
    analytics.trackCartChange(total);
  }
});

Watching Props ​

React to prop changes by watching through widget():

dart
@override
Widget Function(BuildContext) setup() {
  final props = widget<UserAvatar>();
  watch(() => props.value.userId, (newId, oldId) {
    cache.loadAvatar(newId);
  });
  // ...
}

watchEffect() ​

watchEffect() automatically tracks all reactive dependencies accessed inside its callback:

dart
final firstName = ref('John');
final lastName = ref('Doe');

watchEffect(() {
  // Automatically tracks both refs
  print('Full name: ${firstName.value} ${lastName.value}');
});

firstName.value = 'Jane';  // Prints: "Full name: Jane Doe"
lastName.value = 'Smith';  // Prints: "Full name: Jane Smith"

Use Cases ​

  • Logging and debugging: trace when and why values change
  • Syncing external systems: push state changes to analytics, local storage, etc.
  • Side effects without old values: when you don't need to compare previous state

When to Use Which ​

ScenarioRecommended
Derive a new value from reactive statecomputed()
Need old and new values for a side effectwatch()
Simple side effect, no comparisonwatchEffect()
Explicit control over dependencieswatch()
Track many dependencies automaticallywatchEffect()

Batched Updates ​

Multiple writes inside the same microtask collapse into a single re-execution:

dart
void incrementTwice() {
  count.value++;
  count.value++;
  // The watcher fires once with the final value, not twice
}

If you need intermediate frames, schedule them explicitly with Future.microtask or SchedulerBinding.

Automatic Cleanup ​

All watchers created during setup() are automatically cleaned up when the widget is unmounted. You don't need to manually cancel them — the effectScope handles teardown.

Debugging Tips ​

  • Use debugPrint inside watchEffect to trace when it re-runs and what values triggered it.
  • Keep an eye out for ref.value = ref.value patterns — they trigger unnecessary updates.
  • When the UI does not update, check that you read .value inside the builder or computation.

Next Steps ​

Released under the MIT License.