Derivations, Actions, and Reactions
- Computed properties (alson known as derivations) and their various options
- Actions, with special focus on async actions
- Reactions and the rules governing when MobX reacts
Observable State = (Core-mutable-State) + (Derived-readonly-State)
After all, a reaction in MobX looks at observables and produces side effects.
action = untracked( transaction( allowStateChanges( true, <mutating-function> ) ) )
This combination has the following much-intended effects:
- Reducing excessive notifications
- Improving efficiency by batching up a minial set of notifications
- Minimizing executions of side effects, for observables that change several times in an action
setters are automatically wrapped by an action() by MobX. A setter for a computed-property is not really changing the computed-property directly. Instead, it is the inverse that mutates the dependent observables that make up the computed-property.
class ShoppingCart {
/* ... */
@action
async submit() {
this.asyncState = "pending";
try {
const response = await this.purchaseItems(this.items);
runInAction(() => {
this.asyncState = "completed";
});
} catch (ex) {
console.log(ex);
runInAction(() => {
this.asyncState = "failed";
});
}
}
}
复制代码
runInAction(fn) is just a convenience utility that is equivalent ot action(fn)().
class AuthStore {
@observable loginState = "";
login = flow(function*(username, password) {
this.loginState = "pending";
yield this.initializeEnvironment();
this.loginState = "initialized";
yield this.serverLogin(username, password);
this.loginState = "completed";
yield this.sendAnalytics();
this.loginState = "reported";
yield this.delay(3000);
});
}
const promise = new AuthStore().login();
promise.cancel();
复制代码
Reactions
(Identify type of reaction)
=> Long Running? => No, one-time only => when()
=> Yes => Selectively pick Observables? => No => autorun()
=> Yes => reaction()
复制代码
import { autorun, reaction, when } from "mobx";
const disposer1 = autorun(() => {
/* effect function */
});
const disposer2 = reaction(
() => {
/* tracking function returning data */
},
data => {
/* effect function */
}
);
const disposer3 = when(
() => {
/* predicate function */
},
predicate => {
/* effect function */
}
);
// Dispose pre-maturely
disposer1();
disposer2();
disposer3();
复制代码
Handling Real-World Use Cases
- Form validation
- Page routing
import Validate from "validate.js";
configure({ enforceActions: "strict" });
class UserEnrollmentData {
@observable email = "";
@observable password = "";
@observable firstName = "";
@observable lastName = "";
@observable validating = false;
@observable.ref errors = null;
@observable enrollmentStatus = "none"; // none | pending | completed | failed
/** @desc side effects */
disposeValidation = null;
constructor() {
this.setupValidation();
}
setupValidation() {
this.disposeValidation = reaction(
() => {
const { firstName, lastName, password, email } = this;
return { firstName, lastName, password, email };
},
() => {
this.validateFields(this.getFields());
}
);
}
cleanup() {
this.disposeValidation();
}
@action setField(field, value) {
this[field] = value;
}
getFields() {
const { firstName, lastName, password, email } = this;
return { firstName, lastName, password, email };
}
enroll = flow(function*() {
this.enrollmentStatus = "pending";
try {
// Validation
const fields = this.getFields();
/** @desc Validation is actually a side effect that is triggered any time the fields change, but can also be invoked directly as an action. */
yield this.validateFields(fields);
if (this.errors) {
throw new Error("Invalid fields");
}
// Enrollment
yield enrollUser(fields);
this.enrollmentStatus = "completed";
} catch (err) {
this.enrollmentStatus = "failed";
}
});
validateFields = flow(function*(fields) {
this.validating = true;
this.errors = null;
try {
yield Validate.async(fields, rules);
this.errors = null;
} catch (err) {
this.errors = err;
} finally {
this.validating = false;
}
});
}
复制代码
Special API for Special Cases
-
Direct manipulation with the object API
-
Using inject() and observe() to hook into the internal MobX eventing system
-
Special utility functions and tools that will help in debugging
-
Quick mention of some miscellaneous APIs
-
get(thing, key): Retrieves the value under the key. This key can even be nonexistent. When used in a reaction, it will trigger a re-execution when that key becomes available.
-
set(thing, key, value) or set(thing, { key: value }): sets a value for the key. The second form is better for setting multiple key-value pairs at once. Conceptually, it is very similar to Object.assign(), but with the addition of being reactive.
-
has(thing, key): Gives back a boolean indicating if the key is present.
-
remove(thing, key): Removes the given key and its value.
-
values(thing): Gives an array of values.
-
keys(thing): Gives an array containing all the keys. Note that this only applies to observable objects and maps.
-
entries(thing): Gives back an array of key-value pairs, where each pair is an array of two elements ([key, value])
import {
autorun,
observable,
set,
get,
has,
toJS,
runInAction,
remove,
values,
entries,
keys
} from "mobx";
class Todo {
@observable description = "";
@observable done = false;
constructor(description) {
this.description = description;
}
}
const firstTodo = new Todo("Write Chapter");
const todos = observable.array([firstTodo]);
const todosMap = observable.map({
"Write Chapter": firstTodo
});
// Reactions to track changes
autorun(() => {
console.log(`metadata present: ${has(firstTodo, "metadata")}`);
console.log(get(firstTodo, "metadata"), get(firstTodo, "user"));
console.log(keys(firstTodo));
});
autorun(() => {
// Arrays
const secondTodo = get(todos, 1);
console.log("Second Todo:", toJS(secondTodo));
console.log(values(todos), entries(todos));
});
// Granular changes
runInAction(() => {
set(firstTodo, "metadata", "new Metadata");
set(firstTodo, { metadata: "meta update", user: "Pavan Podila" });
set(todos, 1, new Todo("Get it reviewed"));
});
runInAction(() => {
remove(firstTodo, "metadata");
remove(todos, 1);
});
// metadata present: false
// undefined undefined
// (2) ["description", "done"]
// Second Todo: undefined
// [Todo] [Array(2)]
// metadata present: true
// meta update Pavan Podila
// (4) ["description", "done", "metadata", "user"]
// Second Todo: {description: "Get it reviewed", done: false}
// (2) [Todo, Todo] (2) [Array(2), Array(2)]
// metadata present: false
// undefined "Pavan Podila"
// (3) ["description", "done", "user"]
// Second Todo: undefined
// [Todo] [Array(2)]
复制代码
- disposer = onBecomeObserved(observable, property?: string, listener: () => void)
- disposer = onBecomUnobserved(observable, property?: string, listener: () => void)
import {
onBecomeObserved,
onBecomeUnobserved,
observable,
autorun
} from "mobx";
const obj = observable.box(10);
const cart = observable({
items: [],
totalPrice: 0
});
onBecomeObserved(obj, () => {
console.log("Started observing obj");
});
onBecomeUnobserved(obj, () => {
console.log("Stopped observing obj");
});
onBecomeObserved(cart, "totalPrice", () => {
console.log("Started observing cart.totalPrice");
});
onBecomeUnobserved(cart, "totalPrice", () => {
console.log("Stopped observing cart.totalPrice");
});
const disposer = autorun(() => {
console.log(obj.get(), `Cart total: ${cart.totalPrice}`);
});
setTimeout(disposer);
obj.set(20);
cart.totalPrice = 100;
// Started observing obj
// Started observing cart.totalPrice
// 10 "Cart total: 0"
// 20 "Cart total: 0"
// 20 "Cart total: 100"
// Stopped observing cart.totalPrice
// Stopped observing obj
复制代码
diposer = intercept(observable, property?, interceptor: (change = { type, object, newValue, oldValue }) => change | null)
Inside the interceptor callback, you have the opportunity to finalize the type of change you actually want to apply.
- Return null and discard the change
- Update with a different value
- Throw an error indicating an exceptional value
- Return as-is and apply the change
import { intercept, observable } from "mobx";
const theme = observable({
color: "light",
shades: []
});
const disposer = intercept(theme, "color", change => {
console.log("Intercepting:", change);
// Cannot unset value, so discard this change
if (!change.newValue) {
return null;
}
// Handle shorthand values
const newTheme = change.newValue.toLowerCase();
if (newTheme === "l" || newTheme === "d") {
change.newValue = newTheme === "l" ? "light" : "dark"; // setthe correct value
return change;
} // check for a valid theme
const allowedThemes = ["light", "dark"];
const isAllowed = allowedThemes.includes(newTheme);
if (!isAllowed) {
throw new Error(`${change.newValue} is not a valid theme`);
}
return change; // Correct value so return as-is
});
复制代码
observe(observable, property?, observer: (change) => {})
import { observe, observable } from "mobx";
const theme = observable({
color: "light",
shades: []
});
const disposer = observe(theme, "color", change => {
console.log(
`Observing ${change.type}`,
change.oldValue,
"-->",
change.newValue,
"on",
change.object
);
});
theme.color = "dark";
复制代码
The signature is exactly like intercept(), but the behavior is quite different. observe() is invoked after the change has been applied to the observable.
An interesting characteristic is that observe() is immune to transactions. What this means is that the observer callback is invoked immediately after a mutation and does not wait until the transaction completes.
If you wanted to observe changes happening across all observables without having to individually set up the observe() handlers.
disposer = spy*(listner: (event = { type = update | add | delete | create | action | reaction | compute | error , name , oldValue, newValue, spyReportStart , spyReportEnd , ... }) => {})
trace() is a utility that is specifically focused on computed properties, reactions, and component renders.
trace(thing?, property?, enterDebugger? /_ a Boolean flag indicating whether you want to step into the debugger automatically _/)
Visual debugging with mobx-react-devtools
import DevTools from "mobx-react-devtools";
import React from "react";
export class MobXBookApp extends React.Component {
render() {
return (
<Fragment>
<DevTools />
<RootAppComponent />
</Fragment>
);
}
}
复制代码
A few other APIs:
- Querying the reactive system: isObservableObject(thing), isObservableArray(thing), isObservableMap(thing), isObservable(thing), isObservableProp(thing, property?), isBoxedObservable(thing), isAction(func), isComputed(thing), isComputedProp(thing, property?)
- Give you the internal representation of the observables and reactions: getAtom(thing, property?), getDependencyTree(thing, property?), getObserverTree(thing, property?)
Exploring mobx-utils and mobx-state-tree
- mobx-utils for a tool belt of utility functions
- mobx-state-tree(MST) for an opinionated MobX
mobx-utils is an NPM package that gives you several standard utilities to handle common use cases in MobX.
- fromPromise()
- lazyObservable()
- fromResource()
- now()
- createViewModel()
newPromise : { state: 'pending' | 'fulfilled' | 'rejected', value, case({pending, fulfilled, rejected}) } = fromPromise(promiseLike)
import { fromPromise, PENDING, FULFILLED, REJECTED } from "mobx-utils";
class Worker {
operation = null;
start() {
this.operation = fromPromise(this.performOperation());
}
p;
erformOperation() {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
clearTimeout(timeoutId);
Math.random() > 0.25
? resolve("200 OK")
: reject(new Error("500 FAIL"));
}, 1000);
});
}
}
import { fromPromise, PENDING, FULFILLED, REJECTED } from "mobx-utils";
import { observer } from "mobx-react";
import React, { Fragment } from "react";
import { CircularProgress, Typography } from "@material-ui/core/es/index";
@observer
export class FromPromiseExample extends React.Component {
worker;
constructor(props) {
super(props);
this.worker = new Worker();
this.worker.start();
}
render() {
const { operation } = this.worker;
return operation.case({
[PENDING]: () => (
<Fragment>
<CircularProgress size={50} color={"primary"} />
<Typography variant={"title"}>
Operation in Progress
</Typography>
</Fragment>
),
[FULFILLED]: value => (
<Typography variant={"title"} color={"primary"}>
Operation completed with result: {value}
</Typography>
),
[REJECTED]: error => (
<Typography variant={"title"} color={"error"}>
Operation failed with error: {error.message}
</Typography>
)
});
}
}
复制代码
result = lazyObservable(sink => {}, initialValue)
resource = fromResource(subscriber: sink => {}, unsubscriber: () => {}, initialValue)
viewModel = createViewModel(model)
- To finalize the updated values on the original model, you must call the submit() method of viewModel. To reverse any changes, you can invoke the reset() method. To revert a single property, use resetProperty(propertyName: string).
- To check if viewModel is dirty, use the isDirty property. To check if a single property is dirty, use isPropertyDirty(propertyName: string).
- To get the original model, use the handy model() method.
The advantage of using createViewModel() is that you can treat the whole editing process as a single transaction. It is final only when submit() is invoked. This allows you to cancel out prematurely and retain the original model in its previous state.
class FormData {
@observable name = "<Unnamed>";
@observable email = "";
@observable favoriteColor = "";
}
const viewModel = createViewModel(new FormData());
autorun(() => {
console.log(
`ViewModel: ${viewModel.name}, Model: ${viewModel.model.name}, Dirty: ${
viewModel.isDirty
}`
);
});
viewModel.name = "Pavan";
viewModel.email = "pavan@pixelingene.com";
viewModel.favoriteColor = "orange";
console.log("About to reset");
viewModel.reset();
viewModel.name = "MobX";
console.log("About to submit");
viewModel.submit();
// ViewModel: <Unnamed>, Model: <Unnamed>, Dirty: false
// ViewModel: Pavan, Model: <Unnamed>, Dirty: true
// About to reset...
// ViewModel: <Unnamed>, Model: <Unnamed>, Dirty: false
// ViewModel: MobX, Model: <Unnamed>, Dirty: true
// About to submit...
// ViewModel: MobX, Model: MobX, Dirty: false
复制代码
Mobx Internals
- The layered architecture of MobX
- Atoms and ObservableValues
- Derivations and reactions
- What is Transparent Functional Reactive Programming?
Atoms => ObservableValue, ComputedValue and Derivations, Observable{Object, Array, Map} and APIs
- Atoms: Atoms are the foundation of MobX observables. As the name suggests, they are the atomic pieces of the observable dependency tree. It keeps track of its observers but does not actually store any value.
- ObservableValue, ComputedValue, and Derivations: ObservableValue extends Atom and provides the actual storage. It is also the core implementation of boxed Observables. In parallel, we have derivations and reactions, which are the observers of the atoms. They respond to changes in atoms and schedule reactions. ComputedValue builds upon the derivations and also acts as an observable.
- Observable{Object, Array, Map} and APIs: These data structures buildon top of Observable Value and use it to represent their properties and values. This also acts as the API layer of MobX, the primary means of interfacing with the library from the consumer's standpoint.
The reactive system of MobX is backed by a graph of dependencies that exist between the observables. One observable's value could depend on a set of observables, which in turn could depend on other observables.
An atom serves two purposes:
- Notify when it is read. This is done by calling reportObserved()
- Notify when it is changed. This is done by calling reportChanged()
Internally, an atom keeps track of its observers and informs them of the changes.
class Atom {
observers = [];
reportObserved() {}
reportChanged() {}
/* ... */
}
复制代码
import { autorun, $mobx, getDependencyTree } from "mobx";
const cart = new ShoppingCart();
const disposer = autorun(() => {
console.log(cart.description);
});
const descriptionAtom = cart[$mobx].values.get("description");
console.log(getDependencyTree(descriptionAtom));
// {
// name: 'ShoppingCart@16.description',
// dependencies: [
// { name: 'ShoppingCart@16.items' },
// { name: 'ShoppingCart@16.items' },
// {
// name: 'ShoppingCart@16.coupons',
// dependencies: [
// {
// name: 'CouponManager@19.validCoupons',dependencies: [{ name: 'CouponManager@19.coupons' }],
// },
// ],
// },
// ],
// };
复制代码
createAtom(name, onBecomeObservedHandler, onBecomeUnobservedHandler)
class ComputedValue {
get() {
/* ... */
reportObserved(this);
/* ... */
}
set(value) {
/* rarely applicable */
}
observe(listener, fireImmediately) {}
}
复制代码