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.