Skip to main content

onActionMiddleware

This middleware observes actions in a given tree. It invokes listeners only for top-level actions, not for nested child actions or intermediate flow steps. Life-cycle hooks such as onAttachedToRootStore and its returned disposer are also excluded.

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 cancellation by returning a new result (either 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 ActionCall passed to a listener is not directly serializable. Before storing or sending it, use serializeActionCall. Apply the serialized call with applySerializedActionAndTrackNewModelIds on the server or applySerializedActionAndSyncNewModelIds on clients, as shown 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
},
}

registerActionCallArgumentSerializer(myTypeSerializer)

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.