Skip to content

Commit 9ae8461

Browse files
authored
feat: add scheduler option to autorun and reaction (#979)
1 parent b5ab10e commit 9ae8461

7 files changed

+104
-14
lines changed

mobx/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.4.0
2+
3+
- Add `scheduler` to `reaction` and `autorun` to allow customizing the scheduler used to schedule the reaction. By [@amondnet]((https://github.com/amondnet).
4+
15
## 2.3.3+1 - 2.3.3+2
26

37
- Analyzer fixes

mobx/lib/src/api/reaction.dart

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:mobx/src/api/context.dart';
24
import 'package:mobx/src/core.dart';
35

@@ -6,7 +8,10 @@ import 'package:mobx/src/core.dart';
68
///
79
/// Optional configuration:
810
/// * [name]: debug name for this reaction
9-
/// * [delay]: throttling delay in milliseconds
11+
/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens.
12+
/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used.
13+
/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future.
14+
/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig].
1015
///
1116
/// ```
1217
/// var x = Observable(10);
@@ -27,13 +32,21 @@ ReactionDisposer autorun(Function(Reaction) fn,
2732
{String? name,
2833
int? delay,
2934
ReactiveContext? context,
35+
Timer Function(void Function())? scheduler,
3036
void Function(Object, Reaction)? onError}) =>
3137
createAutorun(context ?? mainContext, fn,
32-
name: name, delay: delay, onError: onError);
38+
name: name, delay: delay, scheduler: scheduler, onError: onError);
3339

3440
/// Executes the [fn] function and tracks the observables used in it. Returns
3541
/// a function to dispose the reaction.
3642
///
43+
/// Optional configuration:
44+
/// * [name]: debug name for this reaction
45+
/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens.
46+
/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used.
47+
/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future.
48+
/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig].
49+
///
3750
/// The [fn] is supposed to return a value of type T. When it changes, the
3851
/// [effect] function is executed.
3952
///
@@ -43,20 +56,25 @@ ReactionDisposer autorun(Function(Reaction) fn,
4356
/// [fireImmediately] if you want to invoke the effect immediately without waiting for
4457
/// the [fn] to change its value. It is possible to define a custom [equals] function
4558
/// to override the default comparison for the value returned by [fn], to have fined
46-
/// grained control over when the reactions should run.
59+
/// grained control over when the reactions should run. By default, the [mainContext]
60+
/// is used, but you can also pass in a custom [context].
61+
/// You can also pass in an optional [onError] handler for errors thrown during the [fn] execution.
62+
/// You can also pass in an optional [scheduler] to schedule the [effect] execution.
4763
ReactionDisposer reaction<T>(T Function(Reaction) fn, void Function(T) effect,
4864
{String? name,
4965
int? delay,
5066
bool? fireImmediately,
5167
EqualityComparer<T>? equals,
5268
ReactiveContext? context,
69+
Timer Function(void Function())? scheduler,
5370
void Function(Object, Reaction)? onError}) =>
5471
createReaction<T>(context ?? mainContext, fn, effect,
5572
name: name,
5673
delay: delay,
5774
equals: equals,
5875
fireImmediately: fireImmediately,
59-
onError: onError);
76+
onError: onError,
77+
scheduler: scheduler);
6078

6179
/// A one-time reaction that auto-disposes when the [predicate] becomes true. It also
6280
/// executes the [effect] when the predicate turns true.

mobx/lib/src/core/reaction_helper.dart

+15-8
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,24 @@ class ReactionDisposer {
2323
/// An internal helper function to create a [autorun]
2424
ReactionDisposer createAutorun(
2525
ReactiveContext context, Function(Reaction) trackingFn,
26-
{String? name, int? delay, void Function(Object, Reaction)? onError}) {
26+
{String? name,
27+
int? delay,
28+
Timer Function(void Function())? scheduler,
29+
void Function(Object, Reaction)? onError}) {
2730
late ReactionImpl rxn;
2831

2932
final rxnName = name ?? context.nameFor('Autorun');
33+
final runSync = scheduler == null && delay == null;
3034

31-
if (delay == null) {
35+
if (runSync) {
3236
// Use a sync-scheduler.
3337
rxn = ReactionImpl(context, () {
3438
rxn.track(() => trackingFn(rxn));
3539
}, name: rxnName, onError: onError);
3640
} else {
37-
// Use a delayed scheduler.
38-
final scheduler = createDelayedScheduler(delay);
41+
// Use a scheduler or delayed scheduler.
42+
final schedulerFromOptions =
43+
scheduler ?? (delay != null ? createDelayedScheduler(delay) : null);
3944
var isScheduled = false;
4045
Timer? timer;
4146

@@ -46,7 +51,7 @@ ReactionDisposer createAutorun(
4651
timer?.cancel();
4752
timer = null;
4853

49-
timer = scheduler(() {
54+
timer = schedulerFromOptions!(() {
5055
isScheduled = false;
5156
if (!rxn.isDisposed) {
5257
rxn.track(() => trackingFn(rxn));
@@ -69,6 +74,7 @@ ReactionDisposer createReaction<T>(
6974
int? delay,
7075
bool? fireImmediately,
7176
EqualityComparer<T>? equals,
77+
Timer Function(void Function())? scheduler,
7278
void Function(Object, Reaction)? onError}) {
7379
late ReactionImpl rxn;
7480

@@ -77,8 +83,9 @@ ReactionDisposer createReaction<T>(
7783
final effectAction =
7884
Action((T? value) => effect(value as T), name: '$rxnName-effect');
7985

80-
final runSync = delay == null;
81-
final scheduler = delay != null ? createDelayedScheduler(delay) : null;
86+
final runSync = scheduler == null && delay == null;
87+
final schedulerFromOptions =
88+
scheduler ?? (delay != null ? createDelayedScheduler(delay) : null);
8289

8390
var firstTime = true;
8491
T? value;
@@ -124,7 +131,7 @@ ReactionDisposer createReaction<T>(
124131
timer?.cancel();
125132
timer = null;
126133

127-
timer = scheduler!(() {
134+
timer = schedulerFromOptions!(() {
128135
isScheduled = false;
129136
if (!rxn.isDisposed) {
130137
reactionRunner();

mobx/lib/version.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
22

33
/// The current version as per `pubspec.yaml`.
4-
const version = '2.3.3+2';
4+
const version = '2.4.0';

mobx/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: mobx
2-
version: 2.3.3+2
2+
version: 2.4.0
33
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."
44

55
repository: https://github.com/mobxjs/mobx.dart

mobx/test/autorun_test.dart

+34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:fake_async/fake_async.dart';
24
import 'package:mobx/mobx.dart';
35
import 'package:mocktail/mocktail.dart' as mock;
@@ -101,6 +103,38 @@ void main() {
101103
dispose();
102104
});
103105

106+
test('with custom scheduler', () {
107+
late Function dispose;
108+
const delayMs = 5000;
109+
110+
final x = Observable(10);
111+
var value = 0;
112+
113+
fakeAsync((async) {
114+
dispose = autorun((_) {
115+
value = x.value + 1;
116+
}, scheduler: (f) {
117+
return Timer(const Duration(milliseconds: delayMs), f);
118+
}).call;
119+
120+
async.elapse(const Duration(milliseconds: 2500));
121+
122+
expect(value, 0); // autorun() should not have executed at this time
123+
124+
async.elapse(const Duration(milliseconds: 2500));
125+
126+
expect(value, 11); // autorun() should have executed
127+
128+
x.value = 100;
129+
130+
expect(value, 11); // should still retain the last value
131+
async.elapse(const Duration(milliseconds: delayMs));
132+
expect(value, 101); // should change now
133+
});
134+
135+
dispose();
136+
});
137+
104138
test('with pre-mature disposal in tracking function', () {
105139
final x = Observable(10);
106140

mobx/test/reaction_test.dart

+27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:fake_async/fake_async.dart';
24
import 'package:mobx/mobx.dart' hide when;
35
import 'package:mobx/src/core.dart';
@@ -109,6 +111,31 @@ void main() {
109111
});
110112
});
111113

114+
test('works with scheduler', () {
115+
final x = Observable(10);
116+
var executed = false;
117+
118+
final d = reaction((_) => x.value > 10, (isGreaterThan10) {
119+
executed = true;
120+
}, scheduler: (fn) => Timer(const Duration(milliseconds: 1000), fn));
121+
122+
fakeAsync((async) {
123+
x.value = 11;
124+
125+
// Even though tracking function has changed, effect should not be executed
126+
expect(executed, isFalse);
127+
async.elapse(const Duration(milliseconds: 500));
128+
expect(
129+
executed, isFalse); // should still be false as 1s has not elapsed
130+
131+
async.elapse(
132+
const Duration(milliseconds: 500)); // should now trigger effect
133+
expect(executed, isTrue);
134+
135+
d();
136+
});
137+
});
138+
112139
test('that fires immediately', () {
113140
final x = Observable(10);
114141
var executed = false;

0 commit comments

Comments
 (0)