jolt 2.1.0
jolt: ^2.1.0 copied to clipboard
A lightweight, high-performance reactive signals library for Dart/Flutter with fine-grained updates and intelligent resource management.
Jolt #
Jolt is a lightweight reactive state management library for Dart and Flutter. It provides signals, computed values, effects, async states, and reactive collections with automatic dependency tracking and efficient updates.
Documentation #
Quick Start #
import 'package:jolt/jolt.dart';
void main() {
// Create reactive state
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
// React to changes
Effect(() {
print('Count: ${count.value}, Doubled: ${doubled.value}');
});
count.value = 5; // Prints: "Count: 5, Doubled: 10"
}
Core Concepts #
Signals #
Reactive containers that hold values and notify subscribers when changed:
final counter = Signal(0);
counter.value = 1; // Automatically notifies subscribers
// Access without creating dependency
final currentValue = counter.peek;
// Force notification without changing value
counter.notify();
// Update using updater function
counter.update((value) => value + 1);
// Get read-only view
final readonlyCounter = counter.readonly();
Computed Values #
Derived values that update automatically when dependencies change:
final firstName = Signal('John');
final lastName = Signal('Doe');
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
// Computed values are cached and only recompute when dependencies change
print(fullName.value); // "John Doe"
firstName.value = 'Jane';
print(fullName.value); // "Jane Doe" - automatically updated
// Access cached value without recomputing
final cached = fullName.peekCached;
WritableComputed #
Computed values that can be written to, converting the write to source updates:
final count = Signal(0);
final doubled = WritableComputed(
() => count.value * 2,
(value) => count.value = value ~/ 2,
);
doubled.value = 10; // Automatically updates count to 5
print(count.value); // 5
Effects #
Side-effect functions that run when their dependencies change:
final count = Signal(0);
final List<int> values = [];
Effect(() {
values.add(count.value); // Creates dependency on count
});
count.value = 1; // Effect runs again
count.value = 2; // Effect runs again
// values = [0, 1, 2]
// Effects run immediately by default
// Use lazy: true to defer execution
Effect(() {
print('Deferred effect');
}, lazy: true);
Effect Scopes #
Manage effect lifecycles with automatic cleanup:
final scope = EffectScope((scope) {
Effect(() => print('Reactive effect'));
// Effects are automatically disposed when scope is disposed
});
scope.dispose(); // Cleans up all effects in scope
Watchers #
Watch for changes without creating effects:
final signal = Signal(0);
final List<int> changes = [];
Watcher(
() => signal.value, // Source function
(value, previousValue) {
changes.add(value);
print('Changed from $previousValue to $value');
},
immediately: true, // Run immediately
);
Async Operations #
Handle asynchronous operations with built-in state management:
// From Future
final userSignal = AsyncSignal.fromFuture(fetchUser());
// From Stream
final dataSignal = AsyncSignal.fromStream(dataStream);
// Handle states
Effect(() {
final state = userSignal.value;
if (state.isLoading) print('Loading...');
if (state.isSuccess) print('User: ${state.data}');
if (state.isError) print('Error: ${state.error}');
});
// Using map for cleaner code
final displayText = Computed(() =>
userSignal.value.map(
loading: () => 'Loading...',
success: (data) => 'User: $data',
error: (error, stackTrace) => 'Error: $error',
) ?? 'Unknown'
);
Reactive Collections #
Work with reactive lists, sets, maps, and iterables:
final items = ListSignal(['apple', 'banana']);
final tags = SetSignal({'dart', 'flutter'});
final userMap = MapSignal({'name': 'Alice', 'age': 30});
final range = IterableSignal(() => Iterable.generate(5));
// All mutations trigger reactive updates automatically
items.add('cherry');
items.insert(0, 'orange');
items.removeAt(1);
tags.add('reactive');
tags.remove('dart');
userMap['city'] = 'New York';
userMap.remove('age');
// Direct collection access
final list = items.value; // Get underlying List
items.value = ['new', 'list']; // Replace entire collection
Extension Methods #
Converting to Signals #
Convert existing types to reactive equivalents:
// Convert any value to Signal
final nameSignal = 'Alice'.toSignal();
final countSignal = 42.toSignal();
// Convert collections
final reactiveList = [1, 2, 3].toListSignal();
final reactiveMap = {'key': 'value'}.toMapSignal();
final reactiveSet = {1, 2, 3}.toSetSignal();
final reactiveIterable = Iterable.generate(5).toIterableSignal();
// Convert async sources
final asyncSignal = someFuture.toAsyncSignal();
final streamSignal = someStream.toStreamSignal();
Stream Integration #
Convert reactive values to streams:
final counter = Signal(0);
// Get stream
final stream = counter.stream;
stream.listen((value) => print('Counter: $value'));
// Or use listen convenience method
final subscription = counter.listen(
(value) => print('Counter: $value'),
immediately: true, // Call immediately with current value
);
subscription.cancel(); // Stop listening
Conditional Waiting #
Wait until a reactive value satisfies a condition:
final count = Signal(0);
// Wait until count reaches 5
final future = count.until((value) => value >= 5);
count.value = 1; // Still waiting
count.value = 3; // Still waiting
count.value = 5; // Future completes with value 5
final result = await future; // result is 5
Advanced Features #
Batching Updates #
Batch multiple updates to prevent intermediate notifications:
final signal1 = Signal(1);
final signal2 = Signal(2);
final List<int> values = [];
Effect(() {
values.add(signal1.value + signal2.value);
});
// Without batching: values = [3, 12, 30] (3 updates)
signal1.value = 10;
signal2.value = 20;
// With batching: values = [3, 30] (2 updates)
batch(() {
signal1.value = 10;
signal2.value = 20;
});
Untracked Access #
Access values without creating dependencies:
final signal1 = Signal(1);
final signal2 = Signal(2);
Effect(() {
final tracked = signal1.value; // Creates dependency
final untracked = untracked(() => signal2.value); // No dependency
print('$tracked + $untracked');
});
signal1.value = 10; // Effect runs
signal2.value = 20; // Effect doesn't run
Type-Converting Signals #
Create signals that convert between different types:
import 'package:jolt/tricks.dart';
final count = Signal(0);
final textCount = ConvertComputed(
count,
decode: (int value) => value.toString(),
encode: (String value) => int.parse(value),
);
textCount.value = "42"; // Updates count to 42
print(count.value); // 42
Persistent Signals #
Signals that automatically persist to storage:
import 'package:jolt/tricks.dart';
final theme = PersistSignal(
initialValue: () => 'light',
read: () => SharedPreferences.getInstance()
.then((prefs) => prefs.getString('theme') ?? 'light'),
write: (value) => SharedPreferences.getInstance()
.then((prefs) => prefs.setString('theme', value)),
lazy: false, // Load immediately
writeDelay: Duration(milliseconds: 100), // Debounce writes
);
theme.value = 'dark'; // Automatically saved to storage
Important Notes #
Collection Signals #
- Collection signals automatically notify on mutations
- Direct collection access (
signal.value.add()) requires manualnotify()call - Use collection methods (
signal.add()) for automatic notifications
Async Signals #
- Use
AsyncSignal.fromFuture()for single async operations - Use
AsyncSignal.fromStream()for continuous data streams - Always handle all states: loading, success, error
Effect Dependencies #
- Effects only re-run when tracked dependencies change
- Use
untracked()to access values without creating dependencies - Effects run immediately when created (unless
lazy: true)
Memory Management #
- Always dispose signals and effects when no longer needed
- Use
EffectScopefor automatic cleanup of multiple effects - Disposed signals throw
AssertionErrorwhen accessed
Computed Values #
- Computed values are cached and only recompute when dependencies change
- Use
peekCachedto access cached value without recomputing - Use
peekto recompute without creating dependencies
Related Packages #
Jolt is part of the Jolt ecosystem. Explore these related packages:
| Package | Description |
|---|---|
| jolt_flutter | Flutter widgets: JoltBuilder, JoltSelector, JoltProvider, SetupWidget |
| jolt_hooks | Hooks API: useSignal, useComputed, useJoltEffect, useJoltWidget |
| jolt_flutter_hooks | Declarative hooks for Flutter: useTextEditingController, useScrollController, useFocusNode, etc. |
| jolt_surge | Signal-powered Cubit pattern: Surge, SurgeProvider, SurgeConsumer |
| jolt_lint | Custom lint and code assists: Wrap widgets, convert to/from Signals, Hook conversions |
License #
This project is licensed under the MIT License - see the LICENSE file for details.