Prebuilt middlewares
The MST package ships with some prebuilt middlewares, which serves mainly as examples on how to write your own middleware. The source of each middleware can be found in this github directory, you are encouraged to read them!
The middlewares are bundled separately to keep the core package small, and can be included using:
The middlewares serve as example and are supported on a best effort bases. The goal of these middlewares is that if they are critical to your system, you can simply copy paste them and further tailor them towards your specific needs.
For the exact description of all middleware events offered by MST, see the api docs
Contributing
Feel free to contribute to these middlewares and improve them on your experience. The middlewares must be written in TypeScript. Any additional test for your middleware should be written inside the test folder
simple-action-logger
This is the most basic of middlewares: It logs all direct action invocations. Example:
// .. type definitions ... const store = Store mst storetodos0 // Prints:MST /todos/0/setTitle
For a more sophisticated logger, see action-logger which also logs process invocations and continuations
action-logger
This is a little more sophisticated middlewares: It logs all direct action invocations and also every flow that spawns, returns or throws. Example:
// .. type definitions ... const store = Store mst storetodos0
This will print something like `[MST] -
[MST] #5 action - /todos/0/setTitle
[MST] #5 flow_spawn - /todos/0/setTitle
[MST] #5 flow_spawn - /todos/0/helper2
[MST] #5 flow_return - /todos/0/helper2
[MST] #5 flow_return - /todos/0/setTitle
The number ("#5"
) indicates the id of the original action invocation that lead directly or indirectly to the flow being spawned.
For more details on the meaning of the action types see the middleware docs.
atomic
This middleware rolls back if an (asynchronous) action process fails.
The exception itself is not eaten, but any modifications that are made during the (async) action will be rollback, by reverse applying any pending patches. Can be connected to a model by using either addMiddleware
or decoratore
Example:
const TestModel = types const m = TestModelm
TimeTraveller
This built in model can be used as stand alone store or as part of your state tree and adds time travelling capabilities. It records all emitted snapshots by a tree and exposes the following methods / views:
canUndo: boolean
canRedo: boolean
undo()
redo()
history
: array with all recorded states
The state of the TimeTraveller itself is stored in a Mobx state tree, meaning that you can freely snapshot your state including its history. This means that it is possible to store your app state including the undo stack in for example local storage. (but beware that stringify-ing will not benefit from structural sharing).
Usage inside a state tree:
const Store = types const store = Store // later:if storehistorycanUndo storehistory// etc
Note that the targetPath
is a path relative to the TimeTraveller
instance that will indicate which part of the tree will be snapshotted. Please make sure the targetPath doesn't point to a parent of the time traveller, as that would start recording it's own history..... In other words, targetPath: "../"
-> Boom💥
To instantiate the TimeTraveller
as a stand-alone state tree, pass in the the store through context:
const Store = types const store = Storeconst timeTraveller = TimeTraveller // later:if timeTravellercanUndo timeTraveller// etc
UndoManager
The UndoManager
is the more fine grained TimeTraveller
.
Because it records patches instead of snapshots, it is better at dealing with concurrent and asynchronous processes.
The differences to the TimeTraveller
make it useful to implement end-user undo / redo.
For an in-depth explanation why undo / redo should be patch, not snapshot, based, check out the second half of the React Next talk: MobX-state-tree, React but for data
Differences to the TimeTraveller
:
- It records patches for actions / processes, not snapshots. It's therefore using less memory.
undo
/redo
applies all inverted patches / patches for a recorded action / process instead of snapshots.- An undo state is only comitted to the history if the action / process is finished. Ongoing processes can't be undone.
- Failing processes do not add an undo state.
- Multiple concurrent processes only undo their own changes and don't touch the changes caused by other actions - unlike snapshots would.
- Can be used declaratively within the models.
API:
history: { patches: [], inversePatches [] }[]
canUndo: boolean
canRedo: boolean
undo()
redo()
withoutUndo(() => fn)
patches for actions / processes within the fn are not recorded.withoutUndoFlow(fn*)
patches the fn* are not recorded.startGroup(() => fn)
can be used to start a group, all patches within a group are saved as one history entry.stopGroup()
can be used to stop the recording of patches for the grouped history entry.
Setup and API usage examples:
The setup is very similar to the one of the TimeTraveller
.
The UndoManager
automatically records all the actions within the tree it is attached to.
If you want the history to be a part of your store:
const Store = types let undoManager = {}const setUndoManager = { undoManager = targetStorehistory }const store = Store
To record the changes into a separate tree:
const Store = types let undoManager = {}const setUndoManager = { undoManager = UndoManager }const store = Store
Undo/ Redo:
// if the undoManger is created within another treeconst undo = undoManagercanUndo && undoManagerconst redo = undoManagercanRedo && undoManager
WithoutUndo - within a react component:
... { // the action setPersonName won't be saved onto the history, you could add more than one action. undoManger} { return <div onClick=thissetPersonName> SetPersonName </div> } ...
WithoutUndo - declarative:
const PersonModel = types const StoreModel = types let undoManager = {}const setUndoManager = { undoManager = UndoManager }const Store = StoreModel
WithoutUndoFlow - declarative:
...
StartGroup, StopGroup - within a react component:
... { this; undoManager;} { const view parentNode = thisprops; // only one history entry will be created for the whole dragging // therefore all patches will be merged to one history entry while the group is active undoManager;}...
redux
The Redux 'middleware' is not literally middleware, but provides two useful methods for Redux interoperability:
asReduxStore
asReduxStore(mstStore, middlewares?)
creates a tiny proxy around a MST tree that conforms to the redux store api.
This makes it possible to use MST inside a redux application.
See the redux-todomvc example for more details.
connectReduxDevtools
connectReduxDevtools(remoteDevDependency, mstStore)
connects a MST tree to the Redux devtools. Pass in the remoteDev
dependency to set up the connect (only one at a time). See this example for a setup example.