void_signals 1.0.0 copy "void_signals: ^1.0.0" to clipboard
void_signals: ^1.0.0 copied to clipboard

High-performance signal reactivity library for Dart, based on alien-signals

void_signals logo

void_signals

A high-performance signal reactivity library for Dart, based on alien-signals.

Pub Version License: MIT

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 #

  1. Use peek() for untracked reads instead of wrapping in untrack()
  2. Batch related updates to minimize effect re-runs
  3. Use effect scopes to manage effect lifecycle
  4. Prefer computed over effects for derived state
  5. Place signals at file top-level for better tree shaking

License #

MIT License - see LICENSE for details.

2
likes
140
points
95
downloads

Publisher

unverified uploader

Weekly Downloads

High-performance signal reactivity library for Dart, based on alien-signals

Repository (GitHub)
View/report issues

Topics

#reactivity #signals #state-management #performance

Documentation

API reference

License

MIT (license)

Dependencies

meta

More

Packages that depend on void_signals