jolt 2.1.0 copy "jolt: ^2.1.0" to clipboard
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 #

CI/CD codecov jolt License: MIT

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 #

Official 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 manual notify() 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 EffectScope for automatic cleanup of multiple effects
  • Disposed signals throw AssertionError when accessed

Computed Values #

  • Computed values are cached and only recompute when dependencies change
  • Use peekCached to access cached value without recomputing
  • Use peek to recompute without creating dependencies

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.

2
likes
150
points
982
downloads

Publisher

verified publishervowdemon.com

Weekly Downloads

A lightweight, high-performance reactive signals library for Dart/Flutter with fine-grained updates and intelligent resource management.

Homepage
Repository (GitHub)
View/report issues

Topics

#jolt #state-management #signals

Documentation

API reference

License

MIT (license)

Dependencies

meta, shared_interfaces

More

Packages that depend on jolt