Repository: mobxjs/mobx.dart

Issue:
Allow users to bypass observability system for performance; Avoid unnecessary observable notifications of `@observable` fields of `Store`s; Fix Reaction lacks toString, so cannot see which reaction causes the error; Add StackTrace to reactions in debug mode to easily spot which reaction it is
Close #857 
Close #858 
Close #859
Close #855
Close #864

## Pull Request Checklist


- [ ] If the changes are being made to code, ensure the **version in `pubspec.yaml`** is updated. 
- [ ] Increment the **`major`/`minor`/`patch`/`patch-count`**, depending on the complexity of change
- [ ] Add the necessary **unit tests** to ensure the coverage does not drop
- [ ] Update the **Changelog** to include all changes made in this PR
- [ ] Run the **`set:versions` command** using `npm` or `yarn`. You can find this command in the `package.json` file in the root directory
- [ ] Include the **necessary reviewers** for the PR
- [ ] Update the docs if there are any API changes or additions to functionality




Fix patch:
diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md
index 78be08618..5d8940a71 100644
--- a/mobx/CHANGELOG.md
+++ b/mobx/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.1.4
+
+- Allow users to bypass observability system for performance by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
+- Avoid unnecessary observable notifications of @observable fields of Stores by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
+- Fix Reaction lacks toString, so cannot see which reaction causes the error by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
+- Add StackTrace to reactions in debug mode to easily spot which reaction it is by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
+
 ## 2.1.2 - 2.1.3+1
 
 - Fix tests in dart 2.19 - [@amondnet](https://github.com/amondnet)
diff --git a/mobx/lib/src/api/observable_collections/observable_list.dart b/mobx/lib/src/api/observable_collections/observable_list.dart
index 692bc1bc7..1175127c4 100644
--- a/mobx/lib/src/api/observable_collections/observable_list.dart
+++ b/mobx/lib/src/api/observable_collections/observable_list.dart
@@ -40,6 +40,8 @@ class ObservableList<T>
   final Atom _atom;
   final List<T> _list;
 
+  List<T> get nonObservableInner => _list;
+
   Listeners<ListChange<T>>? _listenersField;
 
   Listeners<ListChange<T>> get _listeners =>
diff --git a/mobx/lib/src/api/observable_collections/observable_map.dart b/mobx/lib/src/api/observable_collections/observable_map.dart
index 842e9fff6..be1cd7d4d 100644
--- a/mobx/lib/src/api/observable_collections/observable_map.dart
+++ b/mobx/lib/src/api/observable_collections/observable_map.dart
@@ -58,6 +58,8 @@ class ObservableMap<K, V>
   final Atom _atom;
   final Map<K, V> _map;
 
+  Map<K, V> get nonObservableInner => _map;
+
   String get name => _atom.name;
 
   Listeners<MapChange<K, V>>? _listenersField;
diff --git a/mobx/lib/src/api/observable_collections/observable_set.dart b/mobx/lib/src/api/observable_collections/observable_set.dart
index d9b561000..65cc56e86 100644
--- a/mobx/lib/src/api/observable_collections/observable_set.dart
+++ b/mobx/lib/src/api/observable_collections/observable_set.dart
@@ -46,6 +46,8 @@ class ObservableSet<T>
   final Atom _atom;
   final Set<T> _set;
 
+  Set<T> get nonObservableInner => _set;
+
   String get name => _atom.name;
 
   Listeners<SetChange<T>>? _listenersField;
diff --git a/mobx/lib/src/core/action.dart b/mobx/lib/src/core/action.dart
index d01cae8d4..63789d96b 100644
--- a/mobx/lib/src/core/action.dart
+++ b/mobx/lib/src/core/action.dart
@@ -1,6 +1,6 @@
 part of '../core.dart';
 
-class Action {
+class Action with DebugCreationStack {
   /// Creates an action that encapsulates all the mutations happening on the
   /// observables.
   ///
@@ -61,6 +61,10 @@ class Action {
       _controller.endAction(runInfo);
     }
   }
+
+  @override
+  String toString() =>
+      'Action(name: $name, identity: ${identityHashCode(this)})';
 }
 
 /// `ActionController` is used to define the start/end boundaries of code which
diff --git a/mobx/lib/src/core/atom.dart b/mobx/lib/src/core/atom.dart
index 56005c7d3..8d92b01a6 100644
--- a/mobx/lib/src/core/atom.dart
+++ b/mobx/lib/src/core/atom.dart
@@ -5,7 +5,7 @@ enum _ListenerKind {
   onBecomeUnobserved,
 }
 
-class Atom {
+class Atom with DebugCreationStack {
   /// Creates a simple Atom for tracking its usage in a reactive context. This is useful when
   /// you don't need the value but instead a way of knowing when it becomes active and inactive
   /// in a reaction.
@@ -42,6 +42,8 @@ class Atom {
 
   DerivationState _lowestObserverState = DerivationState.notTracking;
 
+  bool get isBeingObserved => _isBeingObserved;
+
   // ignore: prefer_final_fields
   bool _isBeingObserved = false;
 
@@ -110,6 +112,9 @@ class Atom {
       }
     };
   }
+
+  @override
+  String toString() => 'Atom(name: $name, identity: ${identityHashCode(this)})';
 }
 
 class WillChangeNotification<T> {
diff --git a/mobx/lib/src/core/atom_extensions.dart b/mobx/lib/src/core/atom_extensions.dart
index ed2a117de..d0b3498ad 100644
--- a/mobx/lib/src/core/atom_extensions.dart
+++ b/mobx/lib/src/core/atom_extensions.dart
@@ -7,6 +7,11 @@ extension AtomSpyReporter on Atom {
   }
 
   void reportWrite<T>(T newValue, T oldValue, void Function() setNewValue) {
+    // Avoid unnecessary observable notifications of @observable fields of Stores
+    if (newValue == oldValue) {
+      return;
+    }
+
     context.spyReport(ObservableValueSpyEvent(this,
         newValue: newValue, oldValue: oldValue, name: name));
 
diff --git a/mobx/lib/src/core/computed.dart b/mobx/lib/src/core/computed.dart
index 717a23b57..6b7e74836 100644
--- a/mobx/lib/src/core/computed.dart
+++ b/mobx/lib/src/core/computed.dart
@@ -181,4 +181,8 @@ class Computed<T> extends Atom implements Derivation, ObservableValue<T> {
     }, context: _context)
         .call;
   }
+
+  @override
+  String toString() =>
+      'Computed<$T>(name: $name, identity: ${identityHashCode(this)})';
 }
diff --git a/mobx/lib/src/core/context.dart b/mobx/lib/src/core/context.dart
index 30ce706b2..1f2aef7b5 100644
--- a/mobx/lib/src/core/context.dart
+++ b/mobx/lib/src/core/context.dart
@@ -343,7 +343,9 @@ class ReactiveContext {
         _resetState();
 
         throw MobXCyclicReactionException(
-            "Reaction doesn't converge to a stable state after ${config.maxIterations} iterations. Probably there is a cycle in the reactive function: $failingReaction");
+            "Reaction doesn't converge to a stable state after ${config.maxIterations} iterations. "
+            "Probably there is a cycle in the reactive function: $failingReaction "
+            "(creation stack: ${failingReaction.debugCreationStack})");
       }
 
       final remainingReactions = allReactions.toList(growable: false);
diff --git a/mobx/lib/src/core/observable.dart b/mobx/lib/src/core/observable.dart
index 4f7f99482..a29230479 100644
--- a/mobx/lib/src/core/observable.dart
+++ b/mobx/lib/src/core/observable.dart
@@ -53,6 +53,8 @@ class Observable<T> extends Atom
     return _value;
   }
 
+  T get nonObservableValue => _value;
+
   set value(T value) {
     _context.enforceWritePolicy(this);
 
@@ -125,4 +127,8 @@ class Observable<T> extends Atom
   @override
   Dispose intercept(Interceptor<T> interceptor) =>
       _interceptors.add(interceptor);
+
+  @override
+  String toString() =>
+      'Observable<$T>(name: $name, identity: ${identityHashCode(this)})';
 }
diff --git a/mobx/lib/src/core/reaction.dart b/mobx/lib/src/core/reaction.dart
index cbfce29e7..5dcce2b83 100644
--- a/mobx/lib/src/core/reaction.dart
+++ b/mobx/lib/src/core/reaction.dart
@@ -6,9 +6,11 @@ abstract class Reaction implements Derivation {
   void dispose();
 
   void _run();
+
+  StackTrace? get debugCreationStack;
 }
 
-class ReactionImpl implements Reaction {
+class ReactionImpl with DebugCreationStack implements Reaction {
   ReactionImpl(this._context, Function() onInvalidate,
       {required this.name, void Function(Object, Reaction)? onError}) {
     _onInvalidate = onInvalidate;
@@ -180,4 +182,8 @@ class ReactionImpl implements Reaction {
 
     _context._notifyReactionErrorHandlers(exception, this);
   }
+
+  @override
+  String toString() =>
+      'Reaction(name: $name, identity: ${identityHashCode(this)})';
 }
diff --git a/mobx/lib/src/utils.dart b/mobx/lib/src/utils.dart
index efbff8905..cd81d806b 100644
--- a/mobx/lib/src/utils.dart
+++ b/mobx/lib/src/utils.dart
@@ -4,3 +4,19 @@ const Duration ms = Duration(milliseconds: 1);
 
 Timer Function(void Function()) createDelayedScheduler(int delayMs) =>
     (fn) => Timer(ms * delayMs, fn);
+
+mixin DebugCreationStack {
+  /// Set the flag to true, to enable [debugCreationStack].
+  /// Otherwise, the stack is always null.
+  static var enable = false;
+
+  /// The stack trace when the object is created
+  final StackTrace? debugCreationStack = () {
+    StackTrace? result;
+    assert(() {
+      if (enable) result = StackTrace.current;
+      return true;
+    }());
+    return result;
+  }();
+}
diff --git a/mobx/lib/version.dart b/mobx/lib/version.dart
index dde8ee573..4a91b916d 100644
--- a/mobx/lib/version.dart
+++ b/mobx/lib/version.dart
@@ -1,4 +1,5 @@
 // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
 
 /// The current version as per `pubspec.yaml`.
-const version = '2.1.3+1';
+
+const version = '2.1.4';
\ No newline at end of file
diff --git a/mobx/pubspec.yaml b/mobx/pubspec.yaml
index e81d01082..013672e6b 100644
--- a/mobx/pubspec.yaml
+++ b/mobx/pubspec.yaml
@@ -1,5 +1,5 @@
 name: mobx
-version: 2.1.3+1
+version: 2.1.4
 description: "MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter apps."
 
 homepage: https://github.com/mobxjs/mobx.dart


Test patch:
diff --git a/mobx/test/action_test.dart b/mobx/test/action_test.dart
index 804fa356d..7df56a2c7 100644
--- a/mobx/test/action_test.dart
+++ b/mobx/test/action_test.dart
@@ -1,4 +1,5 @@
 import 'package:mobx/mobx.dart';
+import 'package:mobx/src/utils.dart';
 import 'package:mocktail/mocktail.dart' as mock;
 import 'package:test/test.dart';
 
@@ -11,6 +12,18 @@ void main() {
   testSetup();
 
   group('Action', () {
+    test('toString', () {
+      final object = Action(() {}, name: 'MyName');
+      expect(object.toString(), contains('MyName'));
+    });
+
+    test('debugCreationStack', () {
+      DebugCreationStack.enable = true;
+      addTearDown(() => DebugCreationStack.enable = false);
+      final object = Action(() {});
+      expect(object.debugCreationStack, isNotNull);
+    });
+
     test('basics work', () {
       final a = Action((String name, String value) {
         expect(name, equals('name'));
diff --git a/mobx/test/all_tests.dart b/mobx/test/all_tests.dart
index fb1c6705e..985418cc8 100644
--- a/mobx/test/all_tests.dart
+++ b/mobx/test/all_tests.dart
@@ -32,6 +32,7 @@ import 'reactive_policies_test.dart' as reactive_policies_test;
 import 'spy_test.dart' as spy_test;
 import 'store_test.dart' as store_test;
 import 'when_test.dart' as when_test;
+import 'atom_test.dart' as atom_test;
 
 void main() {
   observable_test.main();
@@ -71,4 +72,6 @@ void main() {
 
   spy_test.main();
   store_test.main();
+
+  atom_test.main();
 }
diff --git a/mobx/test/atom_extensions_test.dart b/mobx/test/atom_extensions_test.dart
new file mode 100644
index 000000000..003f07fda
--- /dev/null
+++ b/mobx/test/atom_extensions_test.dart
@@ -0,0 +1,61 @@
+import 'package:mobx/mobx.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test(
+      'when write to @observable field with changed value, should trigger notifications for downstream',
+      () {
+    final store = _ExampleStore();
+
+    final autorunResults = <String>[];
+    autorun((_) => autorunResults.add(store.value));
+
+    expect(autorunResults, ['first']);
+
+    store.value = 'second';
+
+    expect(autorunResults, ['first', 'second']);
+  });
+
+  // fixed by #855
+  test(
+      'when write to @observable field with unchanged value, should not trigger notifications for downstream',
+      () {
+    final store = _ExampleStore();
+
+    final autorunResults = <String>[];
+    autorun((_) => autorunResults.add(store.value));
+
+    expect(autorunResults, ['first']);
+
+    store.value = store.value;
+
+    expect(autorunResults, ['first']);
+  });
+}
+
+class _ExampleStore = __ExampleStore with _$_ExampleStore;
+
+abstract class __ExampleStore with Store {
+  @observable
+  String value = 'first';
+}
+
+// This is what typically a mobx codegen will generate.
+mixin _$_ExampleStore on __ExampleStore, Store {
+  // ignore: non_constant_identifier_names
+  late final _$valueAtom = Atom(name: '__ExampleStore.value', context: context);
+
+  @override
+  String get value {
+    _$valueAtom.reportRead();
+    return super.value;
+  }
+
+  @override
+  set value(String value) {
+    _$valueAtom.reportWrite(value, super.value, () {
+      super.value = value;
+    });
+  }
+}
diff --git a/mobx/test/atom_test.dart b/mobx/test/atom_test.dart
new file mode 100644
index 000000000..4498adb98
--- /dev/null
+++ b/mobx/test/atom_test.dart
@@ -0,0 +1,31 @@
+import 'package:mobx/mobx.dart';
+import 'package:mobx/src/utils.dart';
+import 'package:test/test.dart';
+
+import 'util.dart';
+
+void main() {
+  testSetup();
+
+  group('Atom', () {
+    test('toString', () {
+      final object = Atom(name: 'MyName');
+      expect(object.toString(), contains('MyName'));
+    });
+
+    test('debugCreationStack', () {
+      DebugCreationStack.enable = true;
+      addTearDown(() => DebugCreationStack.enable = false);
+      final object = Atom();
+      expect(object.debugCreationStack, isNotNull);
+    });
+
+    test('isBeingObserved', () {
+      final observable = Observable(1, name: 'MyName');
+      expect(observable.isBeingObserved, false);
+      final d = autorun((_) => observable.value);
+      expect(observable.isBeingObserved, true);
+      d();
+    });
+  });
+}
diff --git a/mobx/test/computed_test.dart b/mobx/test/computed_test.dart
index 143b9e1ef..8668cb559 100644
--- a/mobx/test/computed_test.dart
+++ b/mobx/test/computed_test.dart
@@ -1,5 +1,6 @@
 import 'package:collection/collection.dart';
 import 'package:mobx/mobx.dart' hide when;
+import 'package:mobx/src/utils.dart';
 import 'package:mocktail/mocktail.dart';
 import 'package:test/test.dart';
 
@@ -10,6 +11,18 @@ void main() {
   testSetup();
 
   group('Computed', () {
+    test('toString', () {
+      final object = Computed(() {}, name: 'MyName');
+      expect(object.toString(), contains('MyName'));
+    });
+
+    test('debugCreationStack', () {
+      DebugCreationStack.enable = true;
+      addTearDown(() => DebugCreationStack.enable = false);
+      final object = Computed(() {});
+      expect(object.debugCreationStack, isNotNull);
+    });
+
     test('basics work', () {
       final x = Observable(20);
       final y = Observable(10);
diff --git a/mobx/test/observable_list_test.dart b/mobx/test/observable_list_test.dart
index f245f126a..52005e371 100644
--- a/mobx/test/observable_list_test.dart
+++ b/mobx/test/observable_list_test.dart
@@ -627,6 +627,23 @@ void main() {
         '[]': (_) => _[0],
         '+': (_) => _ + [100],
       }.forEach(_templateReadTest);
+
+      test('bypass observable system', () {
+        final list = ObservableList<int>();
+
+        int? nonObservableInnerLength;
+        autorun(
+            (_) => nonObservableInnerLength = list.nonObservableInner.length);
+
+        expect(list.nonObservableInner.length, 0);
+        expect(nonObservableInnerLength, equals(0));
+
+        list.add(20);
+
+        expect(list.nonObservableInner.length, 1);
+        expect(nonObservableInnerLength, equals(0),
+            reason: 'should not be observable');
+      });
     });
   });
 
diff --git a/mobx/test/observable_map_test.dart b/mobx/test/observable_map_test.dart
index 01d740eb4..b057d83e8 100644
--- a/mobx/test/observable_map_test.dart
+++ b/mobx/test/observable_map_test.dart
@@ -228,6 +228,22 @@ void main() {
 
       expect(map.containsKey('a'), isTrue);
     });
+
+    test('bypass observable system', () {
+      final map = ObservableMap<int, int>();
+
+      int? nonObservableInnerLength;
+      autorun((_) => nonObservableInnerLength = map.nonObservableInner.length);
+
+      expect(map.nonObservableInner.length, 0);
+      expect(nonObservableInnerLength, equals(0));
+
+      map[10] = 20;
+
+      expect(map.nonObservableInner.length, 1);
+      expect(nonObservableInnerLength, equals(0),
+          reason: 'should not be observable');
+    });
   });
 }
 
diff --git a/mobx/test/observable_set_test.dart b/mobx/test/observable_set_test.dart
index 811de0cc1..cf4180bef 100644
--- a/mobx/test/observable_set_test.dart
+++ b/mobx/test/observable_set_test.dart
@@ -1,4 +1,5 @@
 import 'package:mobx/src/api/observable_collections.dart';
+import 'package:mobx/src/api/reaction.dart';
 import 'package:mobx/src/core.dart';
 import 'package:mocktail/mocktail.dart';
 import 'package:test/test.dart';
@@ -143,6 +144,22 @@ void main() {
         'retainWhere': (m) => m.retainWhere((i) => i < 3),
       }.forEach(runWriteTest);
     });
+
+    test('bypass observable system', () {
+      final set = ObservableSet<int>();
+
+      int? nonObservableInnerLength;
+      autorun((_) => nonObservableInnerLength = set.nonObservableInner.length);
+
+      expect(set.nonObservableInner.length, 0);
+      expect(nonObservableInnerLength, equals(0));
+
+      set.add(10);
+
+      expect(set.nonObservableInner.length, 1);
+      expect(nonObservableInnerLength, equals(0),
+          reason: 'should not be observable');
+    });
   });
 }
 
diff --git a/mobx/test/observable_test.dart b/mobx/test/observable_test.dart
index fe9ea5a92..8e54f3a3c 100644
--- a/mobx/test/observable_test.dart
+++ b/mobx/test/observable_test.dart
@@ -1,4 +1,5 @@
 import 'package:mobx/mobx.dart';
+import 'package:mobx/src/utils.dart';
 import 'package:mocktail/mocktail.dart' as mock;
 import 'package:test/test.dart';
 
@@ -12,6 +13,18 @@ void main() {
   testSetup();
 
   group('observable<T>', () {
+    test('toString', () {
+      final object = Observable(42, name: 'MyName');
+      expect(object.toString(), contains('MyName'));
+    });
+
+    test('debugCreationStack', () {
+      DebugCreationStack.enable = true;
+      addTearDown(() => DebugCreationStack.enable = false);
+      final object = Observable(42);
+      expect(object.debugCreationStack, isNotNull);
+    });
+
     test('basics work', () {
       final x = Observable<int?>(null);
       expect(x.value, equals(null));
@@ -59,6 +72,14 @@ void main() {
         () => context.endBatch()
       ]);
     });
+
+    test('nonObservableValue', () {
+      final x = Observable<int?>(null);
+      expect(x.nonObservableValue, null);
+
+      x.value = 100;
+      expect(x.nonObservableValue, 100);
+    });
   });
 
   group('createAtom()', () {
diff --git a/mobx/test/reaction_test.dart b/mobx/test/reaction_test.dart
index b013a5eef..3fe6feb56 100644
--- a/mobx/test/reaction_test.dart
+++ b/mobx/test/reaction_test.dart
@@ -1,6 +1,7 @@
 import 'package:fake_async/fake_async.dart';
 import 'package:mobx/mobx.dart' hide when;
 import 'package:mobx/src/core.dart';
+import 'package:mobx/src/utils.dart';
 import 'package:mocktail/mocktail.dart';
 import 'package:test/test.dart';
 
@@ -13,6 +14,18 @@ void main() {
   testSetup();
 
   group('Reaction', () {
+    test('toString', () {
+      final object = ReactionImpl(mainContext, () => null, name: 'MyName');
+      expect(object.toString(), contains('MyName'));
+    });
+
+    test('debugCreationStack', () {
+      DebugCreationStack.enable = true;
+      addTearDown(() => DebugCreationStack.enable = false);
+      final object = ReactionImpl(mainContext, () => null, name: 'MyName');
+      expect(object.debugCreationStack, isNotNull);
+    });
+
     test('basics work', () {
       var executed = false;
       final x = Observable(10);