Action Processors

Sometimes you need to add a new aspect to action processing requiring access to all dispatched actions. Diode provides an extension mechanism to get in between dispatching an action and handling it. These action processors may do whatever they want with the incoming action, such as logging, modifying or cancelling them. The processors can also intercept the return value from the action handler to, for example, capture the modified model into an undo stack.

Usage

To define an action processor, create a class extending the ActionProcessor[M] trait and override the process function.

class LoggingProcessor[M <: AnyRef] extends ActionProcessor[M] {
  var log = Vector.empty[(Long, String)]
  override def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M): ActionResult[M] = {
    // log the action
    log = log :+ (System.currentTimeMillis(), action.toString)
    // call the next processor
    next(action)
  }
}

Add it to the processing chain with addProcessor

val logProcessor = new LoggingProcessor[RootModel]
AppCircuit.addProcessor(logProcessor)

Later you can remove a processor if you don't need it anymore.

AppCircuit.removeProcessor(logProcessor)

Use Cases

Batched Action Processing

To achieve smooth animation in the browser you either need to use CSS transitions or run your animation logic using RAF (requestAnimationFrame). Using RAF guarantees that your code is run at the refresh rate of the display (typically 60Hz) delivering smooth animation. Additionally RAF provides an accurate time value to calculate your animation transitions correctly.

In order for your application to utilize RAF you need to update model in the RAF callback and do so for all animations that are currently running. A convenient way to achieve this is to use a batching action processor that collects dispatched animation actions into a batch and runs them later in the RAF callback.

The RAF example provides a RAFBatcher implementation that does just this. It uses a special marker trait (RAFAction) to identify which actions should be filtered out and batched. When such an action is encountered, it's wrapped inside a RAFWrapper and added to the current batch. If not already done, a RAF callback is requested. When the callback is executed, all batched actions are dispatched and unwrapped when they enter the RAFBatcher's process function. The processor also dispatches a special RAFTimeStamp action containing accurate time for the animation actions. Within your Circuit you should handle this action and update the model with the current timestamp.

val timestampHandler = new ActionHandler(zoomTo(_.now)) {
  override def handle = {
    case RAFTimeStamp(time) =>
      updated(time)
  }
}

The RAFBatcher also takes advantage of Circuit's feature of processing a sequence of actions in one go, calling listeners only after all the actions have been processed. This improves performance and guarantees correct display of animation results. If your application dispatches a lot of small actions, it may make sense to use a batching strategy even when no animations are involved.

Persisting Application State

Diode devtools project contains a convenient action processor for saving and restoring application state. This can be useful when encountering a bug in development and wanting to replicate the same application state later, after the code has been fixed and application reloaded.

The gist of the state persisting action processor is in the process function:

override def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M) = {
  action match {
    case Save(id) =>
      // pickle and save
      save(id, pickle(currentModel))
      ActionResult.NoChange
    case Load(id) =>
      // perform state load and unpickling in an effect
      val effect = Effect(load(id).map(p => Loaded(unpickle(p))))
      ActionResult.EffectOnly(effect)
    case Loaded(newModel) =>
      // perform model update
      ActionResult.ModelUpdate(newModel)
    case _ =>
      next(action)
  }
}

It simply catches a few predefined actions for saving and loading application state and lets everything else pass through to the next processor. Because loading happens asynchronously, it is lifted into an Effect and once loading is completed a Loaded action is dispatched. Within Loaded action handler the application state is actually modified.

The TodoMVC example shows how to use PersistState action processor in an application.

results matching ""

    No results matching ""