Skip to main content

Core Concepts

The Triad of MobX, consisting of Observable, Actions and Reactions

At the heart of MobX are three important concepts: Observables, Actions and Reactions.

Observables

Observables represent the reactive-state of your application. They can be simple scalars to complex object trees. By defining the state of the application as a tree of observables, you can expose a reactive-state-tree that the UI (or other observers in the app) consume.

The term reactive data means that a change to data causes a notification to be fired to every interested observer. It is the application of the classic Observer Design Pattern.

A simple reactive-counter is represented by the following observable:

import 'package:mobx/mobx.dart';

final counter = Observable(0);

More complex observables, such as classes, can be created as well.

class Counter {
Counter() {
increment = Action(_increment);
}

final _value = Observable(0);
int get value => _value.value;

set value(int newValue) => _value.value = newValue;
late Action increment;

void _increment() {
_value.value++;
}
}

On first sight, this does look like some boilerplate code which can quickly go out of hand! This is why we added pub to the mix that allows you to replace the above code with the following:

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
@observable
int value = 0;

@action
void increment() {
value++;
}
}

Note the use of annotations to mark the observable properties of the class. Yes, there is some header boilerplate here but its fixed for any class. As you build more complex classes this boilerplate will fade away and you will mostly focus on the code within the braces.

Note: Annotations are available via the pub package.

The above code is part of the Counter Example.

For those who have experience with the JavaScript version of MobX, it's important to note that deep observability doesn't apply in the Dart version of MobX. This is because Dart is a statically typed language and there's no support for reflection (no dart:mirrors) in Flutter applications. Therefore, if a complex object is marked as @observable then MobX will not be able to automatically track changes to the object's fields. Should you need change tracking at the field level, it's better to mark them individually with the @observable annotation.

Computed Observables

What can be derived, should be derived. Automatically.

The state of your application consists of core-state and derived-state. The core-state is state inherent to the domain you are dealing with. For example, if you have a Contact entity, the firstName and lastName form the core-state of Contact. However, fullName is derived-state, obtained by combining firstName and lastName.

Such derived state, that depends on core-state or other derived-state is called a Computed Observable. It is automatically kept in sync when its underlying observables change.

State in MobX = Core-State + Derived-State

import 'package:mobx/mobx.dart';

part 'contact.g.dart';

class Contact = ContactBase with _$Contact;

abstract class ContactBase with Store {
@observable
String firstName;

@observable
String lastName;

@computed
String get fullName => '$firstName, $lastName';

}

In the example above fullName is automatically kept in sync if either firstName and/or lastName changes.

Actions

Actions are how you mutate the observables. Rather than mutating them directly, actions add a semantic meaning to the mutations. For example, instead of just doing value++, firing an increment() action carries more meaning. Besides, actions also batch up all the notifications and ensure the changes are notified only after they complete. Thus the observers are notified only upon the atomic completion of the action.

Note that actions can also be nested, in which case the notifications go out when the top-most action has completed.

final counter = Observable(0);

final increment = Action((){
counter.value++;
});

When creating actions inside a class, you can take advantage of annotations!

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
@observable
int value = 0;

@action
void increment() {
value++;
}
}

Reactions

Reactions complete the MobX triad of observables, actions and reactions. They are the observers of the reactive-system and get notified whenever an observable they track is changed. Reactions come in few flavors as listed below. All of them return a ReactionDisposer, a function that can be called to dispose the reaction.

One striking feature of reactions is that they automatically track all the observables without any explicit wiring. The act of reading an observable within a reaction is enough to track it!

The code you write with MobX appears to be literally ceremony-free!

ReactionDisposer autorun(Function(Reaction) fn)

Runs the reaction immediately and also on any change in the observables used inside fn.

import 'package:mobx/mobx.dart';

final greeting = Observable('Hello World');

final dispose = autorun((_){
print(greeting.value);
});

greeting.value = 'Hello MobX';

// Done with the autorun()
dispose();


// Prints:
// Hello World
// Hello MobX

ReactionDisposer reaction<T>(T Function(Reaction) fn, void Function(T) effect)

Monitors the observables used inside the fn() function and runs the effect() when the tracking function returns a different value. Only the observables inside fn() are tracked.

import 'package:mobx/mobx.dart';

final greeting = Observable('Hello World');

final dispose = reaction((_) => greeting.value, (msg) => print(msg));

greeting.value = 'Hello MobX'; // Cause a change

// Done with the reaction()
dispose();


// Prints:
// Hello MobX

ReactionDisposer when(bool Function(Reaction) predicate, void Function() effect)

Monitors the observables used inside predicate() and runs the effect() when it returns true. After the effect() is run, when automatically disposes itself. So you can think of when as a one-time reaction. You can also dispose when() pre-maturely.

import 'package:mobx/mobx.dart';

final greeting = Observable('Hello World');

final dispose = when((_) => greeting.value == 'Hello MobX', () => print('Someone greeted MobX'));

greeting.value = 'Hello MobX'; // Causes a change, runs effect and disposes


// Prints:
// Someone greeted MobX

Future<void> asyncWhen(bool Function(Reaction) predicate)

Similar to when but returns a Future, which is fulfilled when the predicate() returns true. This is a convenient way of waiting for the predicate() to turn true.

final completed = Observable(false);

void waitForCompletion() async {
await asyncWhen(() => completed.value == true);

print('Completed');
}

Observer

One of the most visual reactions in the app is the UI. The Observer widget (which is part of the pub), provides a granular observer of the observables used in its builder function. Whenever these observables change, Observer rebuilds and renders.

Below is the Counter example in its entirety.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
@observable
int value = 0;

@action
void increment() {
value++;
}
}

class CounterExample extends StatefulWidget {
const CounterExample();

@override
CounterExampleState createState() => CounterExampleState();
}

class CounterExampleState extends State<CounterExample> {
final Counter counter = Counter();

@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('MobX Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Observer(
builder: (_) => Text(
'${counter.value}',
style: const TextStyle(fontSize: 40),
)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}

Deeper coverage

There are some articles written by Michel Weststrate which go into the philosophy of MobX. Definitely worth reading them.