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

  1. Keep Signals at Domain/Feature Level: Share business logic signals across multiple screens.
  2. Use setState() for Ephemeral State: Ephemeral UI toggles, accordion collapses, and text input caches stay inside NeonState.
  3. Always Clean Up Effects: Invoke the disposer returned by NeonEffect in onDispose().