Skip to main content


This action middleware invokes a listener for all actions of a given tree. Note that the listener will only be invoked for the topmost level actions, so it won't run for child actions or intermediary flow steps. Also it won't trigger the listener for calls to hooks such as onAttachedToRootStore or its returned disposer.

Its main use is to keep track of top-level actions that can be later replicated via applyAction somewhere else (another machine, etc.).

There are two kinds of possible listeners, onStart and onFinish listeners.

  • onStart listeners are called before the action executes and allow cancelation by returning a new return value (which might be a return or a throw).
  • onFinish listeners are called after the action executes, have access to the action's actual return value and allow overriding by returning a new return value (which might be a return or a throw).

The actions passed as arguments to the listener are not in a serializable format. If you want to ensure that the actual action calls are serializable you should use serializeActionCall over the whole action before sending the action call over the wire / storing them and likewise use applySerializedActionAndTrackNewModelIds (for the server) / applySerializedActionAndSyncNewModelIds (for the clients) before applying it (as seen in the Client/Server Example).

It will return a disposer, which only needs to be called if you plan to early dispose of the middleware.

const disposer = onActionMiddleware(myTodoList, {
onStart(actionCall, actionContext) {
// we could serialize the action call and do something with it
const serializableActionCall = serializeActionCall(myTodoList, actionCall)

// optionally cancel the action by throwing something
return {
result: ActionTrackingResult.Throw,
value: new Error("whatever"),

// or by returning a different value
return {
result: ActionTrackingResult.Return,
value: 42,

// or do nothing / return `undefined` to continue it

onFinish(actionCall, actionContext, ret) {
if (ret.result === ActionTrackingResult.Return) {
// the action succeeded and `ret.value` has the return value
} else if (ret.result === ActionTrackingResult.Throw) {
// the action threw and `ret.value` has the thrown value

// as in above, we can either return an object with what to return / throw
// or do nothing / return `undefined` to continue the action

Action serialization with custom types as arguments

Action serialization (via serializeActionCall and deserializeActionCall) supports many cases by default:

  • Primitives (including undefined, bigint and special number values NaN/+Infinity/-Infinity, but not symbol).
  • Tree nodes as paths if they are under the same root node as the model that holds the action being called.
  • Tree nodes as snapshots if not.
  • Arrays and observable arrays.
  • Date objects as timestamps.
  • Maps and observable maps.
  • Sets and observable sets.
  • Plain objects, observable or not.

However, you might want to serialize an action that passes your custom type as an argument. In this case you can register a custom action serializer:

const myTypeSerializer: ActionCallArgumentSerializer<MyType, JsonCompatibleType> = {
id: "someSerializerUniqueId",

serialize(valueToSerialize, serializeChild, targetRoot) {
if (valueToSerialize instanceof MyType) {
return someJsonCompatibleValue
// let other serializer handle it
return cannotSerialize

deserialize(someJsonCompatibleValue, deserializeChild, targetRoot) {
// return back `MyType` from the JSON compatible value


In this case, whenever an instance of MyType is found as an action argument, then (after using serializeActionCall on the action call) the action argument will be serialized as a SerializedActionCallArgument:

$mobxKeystoneSerializer: "someSerializerUniqueId",
value: someJsonCompatibleValue

Likewise, using deserializeActionCall will transform it back to an instance of MyType.