Signals

Signals#

Signals are the core building block of duxt_signals. They hold a value and notify listeners when changed.

Creating Signals#

Use the signal() function to create a signal:

// Basic types
final count = signal(0);
final name = signal('John');
final isActive = signal(true);

// Complex types
final user = signal<User?>(null);
final items = signal<List<String>>([]);
final config = signal<Map<String, dynamic>>({});

Reading Values#

Call Syntax (Preferred)#

final count = signal(0);
print(count()); // 0

Property Syntax#

print(count.value); // 0

Peek (No Tracking)#

Read without creating a dependency:

print(count.peek()); // 0

Use peek() in event handlers or logging where you don't want to track dependencies.

Writing Values#

Set#

Replace the value:

count.set(5);
// or
count.value = 5;

Update#

Update based on the previous value:

count.update((v) => v + 1);

// Works great with collections
items.update((list) => [...list, 'new item']);
items.update((list) => list.where((i) => i != 'remove').toList());

Listening to Changes#

Subscribe to value changes:

final unsubscribe = count.listen((value) {
  print('Count changed to: $value');
});

count.set(5); // Prints: "Count changed to: 5"

// Stop listening
unsubscribe();
count.set(10); // Nothing printed

Disposing Signals#

Clean up when done:

count.dispose();
print(count.isDisposed); // true

// After dispose, set/update are no-ops
count.set(100); // Does nothing

Change Detection#

Signals only notify when the value actually changes:

final count = signal(5);
var notifications = 0;

count.listen((_) => notifications++);

count.set(5); // Same value - no notification
count.set(5); // Same value - no notification
count.set(6); // Different value - notification!

print(notifications); // 1

Working with Objects#

For objects, signals compare by reference:

final user = signal(User(name: 'John'));

// This triggers notification (new object)
user.set(User(name: 'Jane'));

// This also triggers (even if same data)
user.set(User(name: 'Jane'));

// Use update for in-place modifications
user.update((u) => User(name: u.name.toUpperCase()));

Best Practices#

Keep Signals Focused#

Create separate signals for separate concerns:

// Good
final firstName = signal('');
final lastName = signal('');
final email = signal('');

// Avoid
final formData = signal({'firstName': '', 'lastName': '', 'email': ''});

Use Computed for Derived State#

Don't duplicate state:

final firstName = signal('John');
final lastName = signal('Doe');

// Good - derived
final fullName = computed(() => '${firstName()} ${lastName()}');

// Avoid - duplicated
final fullName = signal('John Doe');
// Now you have to manually keep it in sync

Dispose When Done#

Clean up signals to prevent memory leaks:

class MyComponent {
  final count = signal(0);

  void dispose() {
    count.dispose();
  }
}