Reactive State Management
Neon includes a powerful, zero-dependency reactive state management engine built right into package:neon_framework/state.dart. It combines fine-grained signals with classic declarative component state.
1. Reactive Primitives: NeonSignal<T>
A NeonSignal is an observable reactive primitive that holds a value and automatically notifies listeners whenever its value changes.
Basic Usage:
import 'package:neon_framework/neon.dart';
// Create a reactive signal
final counter = NeonSignal<int>(0);
// Read current value
print(counter.value); // Output: 0
// Mutate value
counter.value = counter.value + 1;
// Or using update():
counter.update((val) => val + 1);
2. Derived State: NeonComputed<T>
NeonComputed creates a derived reactive value whose output is computed from other signals. It automatically tracks its dependencies and recomputes only when an upstream signal changes.
final unitPrice = NeonSignal<double>(49.99);
final quantity = NeonSignal<int>(2);
// Automatically recalculates when unitPrice or quantity changes!
final totalPrice = NeonComputed<double>(() {
return unitPrice.value * quantity.value;
});
print(totalPrice.value); // 99.98
quantity.value = 3;
print(totalPrice.value); // 149.97
3. Side Effects: NeonEffect
NeonEffect runs a side effect whenever any signal it reads changes. It is ideal for persistence, analytics, or logging.
// Runs immediately and whenever 'quantity' changes
final disposeEffect = NeonEffect(() {
print('Logging cart update: Quantity is now ${quantity.value}');
});
// Clean up effect when no longer needed
disposeEffect();
4. Async Data States: NeonAsyncValue<T>
Handling network requests and database queries requires tracking loading, success, and error states. NeonAsyncValue<T> encapsulates this pattern with exhaustive pattern matching:
final userProfile = NeonSignal<NeonAsyncValue<UserProfile>>(NeonAsyncValue.loading());
void loadProfile(String userId) async {
userProfile.value = NeonAsyncValue.loading();
try {
final data = await apiService.fetchUser(userId);
userProfile.value = NeonAsyncValue.data(data);
} catch (err, stack) {
userProfile.value = NeonAsyncValue.error(err, stack);
}
}
Rendering in UI:
@override
NeonWidget build(NeonBuildContext context) {
return userProfile.value.when(
data: (user) => Text('Welcome back, ${user.name}!'),
loading: () => CircularProgressIndicator(),
error: (error, _) => Text('Failed to load profile: $error'),
);
}
5. Local Widget State: setState()
For simple localized state, Neon provides StatefulWidget and NeonState<T>, matching Dart developer ergonomics:
class ToggleCard extends StatefulWidget {
final String title;
const ToggleCard({super.key, required this.title});
@override
NeonState<ToggleCard> createState() => _ToggleCardState();
}
class _ToggleCardState extends NeonState<ToggleCard> {
bool _isExpanded = false;
@override
NeonWidget build(NeonBuildContext context) {
return Card(
child: Column(
children: [
ListTile(
title: Text(widget.title),
trailing: Switch(
key: 'switch-expand',
value: _isExpanded,
onChanged: (val) {
setState(() {
_isExpanded = val;
});
},
),
),
if (_isExpanded)
Padding(
padding: NeonEdgeInsets.all(16.0),
child: Text('Detailed view content displayed here!'),
),
],
),
);
}
}
Best Practices
- Keep Signals at Domain/Feature Level: Share business logic signals across multiple screens.
- Use
setState()for Ephemeral State: Ephemeral UI toggles, accordion collapses, and text input caches stay insideNeonState. - Always Clean Up Effects: Invoke the disposer returned by
NeonEffectinonDispose().