Effects

Effects#

Effects run side effects when signals change. Use them for logging, DOM updates, API calls, or any other action that should happen in response to state changes.

Creating Effects#

final count = signal(0);

effect(() {
  print('Count is now: ${count()}');
});
// Immediately prints: "Count is now: 0"

count.set(5);
// Prints: "Count is now: 5"

Automatic Dependency Tracking#

Effects automatically track which signals they read:

final name = signal('John');
final age = signal(25);

effect(() {
  print('Name: ${name()}');
  // Only tracks name, not age
});

name.set('Jane'); // Effect runs
age.set(26);      // Effect does NOT run

Cleanup#

The effect() function returns a dispose function:

final dispose = effect(() {
  print('Count: ${count()}');
});

count.set(1); // Effect runs
count.set(2); // Effect runs

dispose(); // Stop the effect

count.set(3); // Effect does NOT run

Use Cases#

Logging#

effect(() {
  console.log({
    'user': currentUser()?.toJson(),
    'cart': cartItems().map((i) => i.id).toList(),
  });
});

API Calls#

effect(() {
  final query = searchQuery();
  if (query.isNotEmpty) {
    fetchResults(query).then((results) {
      searchResults.set(results);
    });
  }
});

Local Storage#

effect(() {
  localStorage.setItem('settings', jsonEncode(settings()));
});

DOM Updates (Jaspr)#

effect(() {
  final theme = isDarkMode() ? 'dark' : 'light';
  document.body?.setAttribute('data-theme', theme);
});

Batch Updates#

Use batch() to group multiple signal updates:

final a = signal(1);
final b = signal(2);

effect(() {
  print('a=${a()}, b=${b()}');
});
// Prints: "a=1, b=2"

// Without batch - effect runs twice
a.set(10); // Effect runs
b.set(20); // Effect runs again

// With batch - effect runs once
batch(() {
  a.set(100);
  b.set(200);
});
// Effect runs once: "a=100, b=200"

Untracked Reads#

Use untracked() to read signals without creating dependencies:

effect(() {
  final name = user().name; // Creates dependency
  final config = untracked(() => appConfig()); // No dependency

  print('User: $name, Config: $config');
});

Best Practices#

Keep Effects Focused#

Each effect should do one thing:

// Good
effect(() {
  localStorage.setItem('user', jsonEncode(user()));
});

effect(() {
  analytics.track('user_changed', user());
});

// Avoid
effect(() {
  localStorage.setItem('user', jsonEncode(user()));
  analytics.track('user_changed', user());
  sendToServer(user());
  // Too many responsibilities
});

Avoid Modifying Signals in Effects#

Don't create infinite loops:

// Dangerous!
effect(() {
  count.set(count() + 1); // Infinite loop!
});

// If you need derived state, use computed
final doubled = computed(() => count() * 2);

Dispose When Done#

Always dispose effects when they're no longer needed:

class MyComponent {
  late final void Function() _disposeEffect;

  void init() {
    _disposeEffect = effect(() {
      // ...
    });
  }

  void dispose() {
    _disposeEffect();
  }
}