Skip to content

Common Pitfalls ​

Most Reacton bugs trace back to the same handful of mistakes. This page catalogs each, shows the symptom, and gives a one-line fix.

1. Creating reactons inside build() ​

Symptom. Your app rebuilds frenetically, DevTools shows a new reacton every frame, memory climbs.

Anti-pattern.

dart
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = reacton(0); // ← created every build
    final count = context.watch(counter);
    return Text('$count');
  }
}

Fix. Declare reactons as top-level final variables, or inside a module.

dart
final counter = reacton(0, name: 'counter');

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final count = context.watch(counter);
    return Text('$count');
  }
}

The avoid_reacton_in_build lint rule catches this statically.

2. Calling context.read() inside build() ​

Symptom. Widget never rebuilds when the underlying reacton changes.

Anti-pattern.

dart
@override
Widget build(BuildContext context) {
  final count = context.read(counter); // ← no subscription created
  return Text('$count');
}

Fix. Use context.watch() in build(). Keep context.read() for event handlers where you need a one-time read.

dart
@override
Widget build(BuildContext context) {
  final count = context.watch(counter);          // subscribes + rebuilds
  return ElevatedButton(
    onPressed: () {
      final current = context.read(counter);     // one-off read, no subscription
      logger.info('current: $current');
    },
    child: Text('$count'),
  );
}

The avoid_read_in_build lint rule catches this.

3. Mutating a list/map in place ​

Symptom. You called list.add(...) and widgets didn't rebuild.

Anti-pattern.

dart
final todos = reacton<List<Todo>>([], name: 'todos');

void add(Todo t) {
  final list = store.get(todos);
  list.add(t);                     // ← same list reference
  store.set(todos, list);          // set with the same ref; equality returns true
}

Fix. Either return a new list…

dart
store.update(todos, (list) => [...list, t]);

…or use observable collections, which emit granular events on mutation:

dart
final todos = reactonList<Todo>([], name: 'todos');
store.mutateList(todos, (list) => list.add(t));

4. Using computed() where selector() would do ​

Symptom. A widget rebuilds on every state change, even ones that do not affect the value you care about.

Anti-pattern.

dart
final userName = computed((read) => read(userReacton).name);

This works, but if userReacton has many fields and most changes do not affect name, every widget reading userName still re-runs the compute.

Fix. selector() applies equality on the selected sub-value, so it only propagates when the slice actually changes.

dart
final userName = selector(userReacton, (u) => u.name);

Rule of thumb: pure projection of a sub-value → selector. Derivation that combines multiple reactons → computed.

5. Effects that write to their own dependencies ​

Symptom. Infinite loop, stack overflow, app freezes.

Anti-pattern.

dart
createEffect((read) {
  final count = read(counter);
  store.set(counter, count + 1); // ← effect writes to what it reads
});

Fix. Break the cycle. Derive via computed() if you just need a value, or trigger the write from a user event / timer / another reacton that you do not read in the effect.

6. Forgetting the cleanup from effects ​

Symptom. Subscriptions, timers, stream listeners leak.

Anti-pattern.

dart
createEffect((read) {
  final sub = stream.listen((e) => ...);
  // ← nothing returned
});

Fix. Return a cleanup callback.

dart
createEffect((read) {
  final sub = stream.listen((e) => ...);
  return () => sub.cancel();
});

7. Wrapping every reacton in async ​

Symptom. Loading spinners for synchronous data. Complicated pattern matching for values that cannot fail.

Anti-pattern.

dart
final feature = asyncReacton((read) async => Feature.defaults());

Fix. reacton() and computed() are synchronous and free. Use AsyncValue only when the value truly comes from a Future/Stream — network, disk, cross-isolate.

8. Nesting ReactonScope unnecessarily ​

Symptom. Child scope loses access to parent reactons, or worse — the same reacton is read from two different stores.

Rule.

  • Top-level app state lives in one ReactonScope at the root.
  • Feature-scoped, disposable state (e.g. a wizard flow, a modal) is a good case for a nested ReactonScope with its own store.

Nesting works, but nest with intent. If in doubt, have one scope.

9. Reading reactons in initState / dispose ​

Symptom. State lifecycle events run once; subsequent changes never reach your code.

Fix. Use context.read() inside initState / didChangeDependencies for a one-time read, and a ReactonListener widget for ongoing side-effect callbacks:

dart
ReactonListener(
  reacton: errorReacton,
  listener: (ctx, err) {
    if (err != null) showSnack(ctx, err);
  },
  child: const MyView(),
);

10. Persisting non-serializable state ​

Symptom. PersistenceMiddleware throws on app start, or restored values are junk.

Fix. Every persisted reacton needs a Serializer. Use PrimitiveSerializer for primitives, JsonSerializer for Dart classes with toJson/fromJson, and hand-roll a serializer for anything exotic. Non-serializable values (closures, BuildContext, futures) must never be persisted.

See Persistence for the full contract.

11. Testing without TestReactonStore ​

Symptom. Tests pass locally but fail in CI because of stale global state.

Fix. Every test gets its own TestReactonStore, with overrides for dependencies:

dart
test('doubled reflects counter', () {
  final store = TestReactonStore(overrides: [
    ReactonTestOverride(counter, 5),
  ]);

  expectReacton(store, doubled).toHaveValue(10);
});

See Unit Testing.

12. Using context.set() inside a computed ​

Symptom. Works in dev, crashes or produces glitches in release.

Anti-pattern.

dart
final total = computed((read) {
  final items = read(cart);
  store.set(totalCache, items.length); // ← side effect inside compute
  return items.length;
});

Fix. computed must be pure. Move the side effect into an effect(), or remove the cache (Reacton's graph engine already memoizes computed values).

Lint rules that catch most of these ​

Enable reacton_lint in your analysis_options.yaml:

yaml
analyzer:
  plugins:
    - custom_lint

custom_lint:
  rules:
    - avoid_reacton_in_build
    - avoid_read_in_build
    - prefer_computed

See Lint Rules for the full list, including auto-fixes.

Released under the MIT License.