Skip to content

Core Concepts

Signals

A signal is a reactive container. Read it by calling it, write with .set(...).

dart
final name = signal(context, 'Oref');
name(); // read
name.set('Oref v2'); // write

Nullable Context (Widget-bound vs Global)

All core APIs accept BuildContext?. When you pass a widget context, Oref memoizes the reactive node and disposes it with the widget. When you pass null, the node becomes global/standalone and you must manage cleanup.

dart
// Inside a widget build -> auto-bound & auto-disposed
final count = signal(context, 0);
effect(context, () => debugPrint('count: ${count()}'));

// Outside widgets -> pass null and clean up manually
final globalCount = signal<int>(null, 0);
final stop = effect(null, () => debugPrint('global: ${globalCount()}'));

// later...
stop(); // dispose effect

Write API in Practice

Signals are callables, not mutable fields. To update, read then .set(...):

dart
final count = signal(context, 0);

void increment() {
  count.set(count() + 1);
}

void reset() {
  count.set(0);
}

Computed

Computed values derive from signals and cache automatically.

dart
final count = signal(context, 2);
final squared = computed(context, (_) => count() * count());

Writable Computed

A writable computed value can map writes back to a source.

dart
import 'dart:math' as math;

final count = signal<double>(context, 0);
final squared = writableComputed<double>(
  context,
  get: (_) => count() * count(),
  set: (value) => count.set(math.sqrt(value)),
);

Effects

Effects track dependencies and re-run when signals change.

dart
final count = signal(context, 0);

effect(context, () {
  debugPrint('count = ${count()}');
});

Batch and Untrack

Batch multiple updates to avoid extra recomputations:

dart
batch(() {
  a.set(1);
  b.set(2);
});

Use untrack when you want to read without subscribing:

dart
final value = untrack(() => count());

Flutter Examples (from the example app)

dart
class CounterSection extends StatelessWidget {
  const CounterSection({super.key});

  @override
  Widget build(BuildContext context) {
    final count = signal<double>(context, 2);
    final doubled = computed<double>(context, (_) => count() * 2);
    final squared = writableComputed<double>(
      context,
      get: (_) => count() * count(),
      set: (value) {
        final safe = value < 0 ? 0.0 : value;
        count.set(math.sqrt(safe));
      },
    );

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('count: ${count().toStringAsFixed(1)}'),
        Text('doubled (computed): ${doubled().toStringAsFixed(1)}'),
        Text('squared (writable): ${squared().toStringAsFixed(1)}'),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => count.set(count() + 1),
              child: const Text('Increment'),
            ),
            OutlinedButton(
              onPressed: () => squared.set(81),
              child: const Text('Set squared = 81'),
            ),
          ],
        ),
      ],
    );
  }
}
dart
class EffectBatchSection extends StatelessWidget {
  const EffectBatchSection({super.key});

  @override
  Widget build(BuildContext context) {
    final a = signal<int>(context, 1);
    final b = signal<int>(context, 2);
    final sum = computed<int>(context, (_) => a() + b());
    final effectRuns = signal<int>(context, 0);

    effect(context, () {
      sum();
      final current = untrack(() => effectRuns());
      effectRuns.set(current + 1);
    });

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('a: ${a()}  b: ${b()}  sum (computed): ${sum()}'),
        Text('effect runs: ${effectRuns()}'),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => a.set(a() + 1),
              child: const Text('Increment A'),
            ),
            ElevatedButton(
              onPressed: () => b.set(b() + 1),
              child: const Text('Increment B'),
            ),
            OutlinedButton(
              onPressed: () {
                batch(() {
                  a.set(a() + 1);
                  b.set(b() + 1);
                });
              },
              child: const Text('Batch +1 both'),
            ),
          ],
        ),
      ],
    );
  }
}