void_signals 1.0.0
void_signals: ^1.0.0 copied to clipboard
High-performance signal reactivity library for Dart, based on alien-signals
void_signals
A high-performance signal reactivity library for Dart, based on alien-signals.
English | 简体中文
Features #
- ⚡ High Performance: Based on alien-signals, one of the fastest signal implementations
- 🎯 Zero Overhead Abstractions: Uses Dart extension types for zero-cost abstractions
- 🔄 Fine-Grained Reactivity: Only updates what actually changed
- 🧩 Minimal API: Just
signal(),computed(),effect()- that's it! - 📦 Tree Shakable: Only bundle what you use
Installation #
dependencies:
void_signals: ^1.0.0
Quick Start #
import 'package:void_signals/void_signals.dart';
void main() {
// Create a signal
final count = signal(0);
// Create a computed value
final doubled = computed((prev) => count() * 2);
// Create an effect
effect(() {
print('Count: ${count()}, Doubled: ${doubled()}');
});
count.value = 1; // Prints: Count: 1, Doubled: 2
}
Core API #
Signal #
A signal holds a reactive value that notifies subscribers when changed.
// Create a signal with initial value
final name = signal('John');
// Read the value (multiple ways)
print(name.value); // 'John'
print(name()); // 'John' (callable syntax)
// Update the value
name.value = 'Jane';
// Read without tracking (useful in effects)
print(name.peek());
// Check if signal has subscribers
print(name.hasSubscribers); // true/false
Computed #
A computed value is derived from other signals and automatically updates.
final firstName = signal('John');
final lastName = signal('Doe');
// Computed with access to previous value
final fullName = computed((prev) => '${firstName()} ${lastName()}');
print(fullName()); // 'John Doe'
// Update dependencies
firstName.value = 'Jane';
print(fullName()); // 'Jane Doe'
// Previous value is available
final runningSum = computed<int>((prev) => (prev ?? 0) + count());
Effect #
An effect runs automatically when its dependencies change.
final count = signal(0);
// Create an effect - runs immediately
final eff = effect(() {
print('Count is: ${count()}');
});
// Prints: Count is: 0
count.value = 1; // Prints: Count is: 1
// Stop the effect
eff.stop();
count.value = 2; // Nothing printed
Effect Scope #
Group multiple effects for easy cleanup.
final scope = effectScope(() {
effect(() { /* effect 1 */ });
effect(() { /* effect 2 */ });
effect(() { /* effect 3 */ });
});
// Later, stop all effects at once
scope.stop();
Batch #
Batch multiple updates to run effects only once.
final a = signal(1);
final b = signal(2);
effect(() {
print('Sum: ${a() + b()}');
});
// Without batch: would print twice
// With batch: prints once
batch(() {
a.value = 10;
b.value = 20;
});
// Prints: Sum: 30
Untrack #
Read signals without creating dependencies.
effect(() {
// This creates a dependency
print('Count: ${count()}');
// This does NOT create a dependency
final other = untrack(() => otherSignal());
});
Trigger #
Manually trigger subscribers of accessed signals.
final list = signal<List<int>>([]);
// Mutate in place
list.value.add(1);
// Trigger subscribers manually
trigger(() => list());
Async Support #
AsyncValue #
A sealed class representing async states: loading, data, or error.
// All AsyncValue variants:
const AsyncLoading<int>(); // Initial loading state
const AsyncData<int>(42); // Success with value
AsyncError<int>(error, stackTrace); // Error state
// With previous value (for refreshing):
AsyncLoadingWithPrevious<int>(42); // Loading but has previous value
AsyncErrorWithPrevious<int>(error, stackTrace, 42);
// Pattern matching
asyncValue.when(
loading: () => print('Loading...'),
data: (value) => print('Got: $value'),
error: (error, stack) => print('Error: $error'),
);
// Convenient getters
asyncValue.isLoading; // true if loading
asyncValue.hasData; // true if has data
asyncValue.hasError; // true if has error
asyncValue.valueOrNull; // value or null
asyncValue.errorOrNull; // error or null
AsyncComputed #
Computed values for async operations with automatic dependency tracking.
final userId = signal(1);
// Create an async computed that fetches user data
final user = asyncComputed(() async {
final id = userId(); // Tracked synchronously before await
final response = await fetchUser(id);
return response;
});
// Use the async state
print(user().isLoading); // true initially
// When userId changes, user automatically refetches
userId.value = 2; // Triggers new computation
// Access the future for async dependencies
final derived = asyncComputed(() async {
final u = await user.future; // Creates dependency
return 'Hello, ${u.name}!';
});
// Manual refresh
user.refresh();
// Cleanup
user.dispose();
StreamComputed #
Subscribe to streams with automatic lifecycle management.
final filter = signal('active');
// Create a stream computed
final items = streamComputed(() {
return database.watchItems(filter: filter()); // Tracked dependency
});
// Access stream state
items().when(
loading: () => 'Loading...',
data: (value) => 'Items: $value',
error: (e, _) => 'Error: $e',
);
// When filter changes, automatically resubscribes to new stream
filter.value = 'archived';
// Cleanup
items.dispose();
combineAsync #
Combine multiple async values into one.
final user = asyncComputed(() => fetchUser(userId()));
final posts = asyncComputed(() => fetchPosts(userId()));
final comments = asyncComputed(() => fetchComments(userId()));
// Combine all async values
final combined = combineAsync(
[user, posts, comments],
(values) => UserProfile(
user: values[0] as User,
posts: values[1] as List<Post>,
comments: values[2] as List<Comment>,
),
);
// combined is loading until all sources complete
combined.when(
loading: () => showSpinner(),
data: (profile) => showProfile(profile),
error: (e, _) => showError(e),
);
Type Checking #
final s = signal(1);
final c = computed((p) => s() * 2);
final e = effect(() => print(s()));
final scope = effectScope(() {});
isSignal(s); // true
isComputed(c); // true
isEffect(e); // true
isEffectScope(scope); // true
Advanced Usage #
Nested Computed Values #
final a = signal(1);
final b = computed((p) => a() * 2);
final c = computed((p) => b() + 1);
print(c()); // 3
a.value = 5;
print(c()); // 11
Diamond Dependencies #
Handles diamond-shaped dependency graphs efficiently:
final source = signal(1);
final left = computed((p) => source() * 2);
final right = computed((p) => source() * 3);
final combined = computed((p) => left() + right());
// Updates only once when source changes
source.value = 2; // combined recalculates only once
Conditional Dependencies #
final showDetails = signal(false);
final details = signal('Secret');
effect(() {
if (showDetails()) {
print('Details: ${details()}');
} else {
print('Hidden');
}
});
// Effect doesn't re-run when details changes (not a dependency yet)
details.value = 'New Secret';
// Now details becomes a dependency
showDetails.value = true;
Low-Level API #
For advanced use cases, the library exposes low-level functions:
import 'package:void_signals/void_signals.dart';
// Get/set the active subscriber
final prevSub = setActiveSub(null);
// ... do untracked work
setActiveSub(prevSub);
// Manually control batching
startBatch();
try {
// Multiple updates
} finally {
endBatch();
}
// Access reactive flags
final flags = someNode.flags;
if (flags.isDirty) { /* ... */ }
Performance Tips #
- Use
peek()for untracked reads instead of wrapping inuntrack() - Batch related updates to minimize effect re-runs
- Use effect scopes to manage effect lifecycle
- Prefer computed over effects for derived state
- Place signals at file top-level for better tree shaking
Related Packages #
- void_signals_flutter - Flutter bindings
- void_signals_hooks - Flutter hooks integration
License #
MIT License - see LICENSE for details.