Skip to content

Using Handler Middleware

Mike Thompson edited this page Aug 30, 2016 · 79 revisions

In v0.8.0 of re-frame, Middleware was replaced by Interceptors.
So this document is no longer current, and has been retained only as a record.
Current docs can be found here

-- re-frame allows you to wrap event handlers in Middleware.

In response, we might wonder "What is Middleware?" and "Why do I need it"?

Why Middleware?

We want simple event handlers, right? As simple as possible. Middleware helps deliver this goal.

Middleware are useful for handling "cross-cutting" concerns like undoing, tracing and validation. They can factor out commonality, hide complexity and introduce further steps into the "Derived Data, Flowing" story promoted by re-frame.

What's Middleware?

I could tell you this:

The term Middleware refers to a set of conventions that programmers adhere to so as to flexibly create domain-specific function pipelines.

or if I saw that you were clearly a masochist:

A Middleware is an Endofunctor, and a collection of Middleware with clojure.core/comp is a Monoid.

Terse, accurate, marvelous - and completely useless ... unless you already know what Middleware is.

I think the best way to understand Middlewares is in two steps:

  1. see how to use them, then
  2. see how to write them

This page deals with "using them", and there's a 2nd page on "writing them".

The Name "Middleware"

The name Middleware is misleading.

It is the exact opposite of what it should be because Middleware has nothing to do with the middle, and everything to do with the outside. Seriously, it should be called Outsideware. Unfortunately I don't have enough seniority to change the entire software industry (yet!), so we'll stick with this annoying name for the moment.

Think about it this way: you have written a handler, which is like a piece of ham. And if you use a Middleware, it will be like bread either side of your ham, which makes the sandwich.

And if you have two pieces of Middleware, it is like you put another pair of bread slices around the outside of the existing sandwich to make a sandwich of the sandwich. Now it is a very thick sandwich. Middleware wraps around the outside.

Here is an N point plan to achieve Sandwich enlightenment:

1. Notice The Built-In Middleware

re-frame comes with free middleware:

  • pure: allows you to write pure handlers. This middleware is so critical that it is automatically applied by register-handler. On the other hand, because it is automatically supplied, you can almost ignore it.
  • undoable: allows you to store away the current value in app-db, so you can later undo!
  • enrich: this one gives us more derived data flowing.
  • debug: report each event as it is processed. Shows incremental clojure.data/diff reports.
  • path: a convenience. Simplifies our handlers.
  • trim-v: a convenience. More readable handlers.
  • after: perform side effects, after a handler has run. Eg: use it to report if the data in app-db matches a schema.

To use them, require them like this:

(ns my.core
  (:require
    [re-frame.core :refer [debug undoable path]])

2. Realise Middleware Are Functions

They are functions which turn handlers, into handlers.

You give a handler as a parameter to Middleware, and it will return a handler - a tweaked version of the handler you passed in.

You could supply this tweaked handler to re-frame.core/register-handler if you wanted to. It looks like a handler, it quacks like a handler. Yep, its a regular handler.

So middleware is:

handler -> handler

;; which expands to
(db -> event -> db) -> (db -> event -> db)

3. See An Example

We'll start with a middleware called trim-v which is useful if you are easily offended by underscores.

Say our Components need to do this kind of thing:

(dispatch [:delete-item 42])

So, we write a handler:

(defn delete-handler
  [db [_  key-to-delete]]  ;;  2nd param is destructuring like [:delete-item 42]
  (dissoc db key-to-delete))

Event handlers take two parameters:

  • the current state of the database, called db above
  • the event vector (given to dispatch) which you can see above is destructured: [_ key-to-delete]. It would be something like [:delete-item 42] and we want to ignore the first element, and pick up the second. Hence the underscore in the first place.

and they return the new state of the database.

We register this handler:

(register-handler :delete-item  delete-handler)

Done. Working.

Except, remember we don't like underscores. Really don't like them. Just look at it there in the handler above, almost mocking us with its offensive lack of aesthetic beauty.

We want to write our handler like this:

(defn delete-handler
  [db  [key-to-delete]]      ;; bliss, not an underscore in sight
  (dissoc db key-to-delete))

But how? The re-frame router calls handlers with the entire event vector and that means the 1st element is a bit useless, but there's no getting away from its existence.

Middleware to the rescue. We do this:

(register-handler :delete-item (trim-v delete-handler))    ;; <== trim-v used here

trim-v is Middleware, right? Which means:

  • it is a function
  • you pass in a handler, and it returns a handler
(trim-v delete-handler)      ;; returns a handler (which wraps delete-handler)

trim-v is the bread wrapping around our delete-handler ham.

When the re-frame router calls the registered handler for :delete-handler it will now be calling a handler (created by trim-v) which wraps our handler, and which gets rid of the first annoying element of the event vector, before it gives it our handlers.

Do you remember back in the day, when you thought OO was cool? So young and foolish. Your head was full of GOF Design Patterns ... like "The Adapter Pattern"? Well, trim-v is adapter-creating, but in functional clothing. Lucky those old days weren't a complete loss, right?

But not all Middleware is adaptor creating.

Before we move on, be aware that there's also this way to register:

(register-handler          
   :delete-item
   trim-v                  ;; <== middleware here
   delete-handler)         ;; <== real handler here

That's a 3-arity version of register-handler which takes the "wrapping" middleware as the 2nd parameter.

4. Grasp Composition

The nice thing about Middleware is that multiple of them can be composed into a multi-step pipeline.

Each individual piece of Middleware can do one simple job, but multiple of them can be combined in myriad ways.

Middleware composes via clojure.core/comp

(def trim-debug   (comp trim-v debug))     ;; comp is given two middleware

trim-debug is Middleware. For the moment, forget that it is a pipeline of two Middleware. See it just as you saw trim-v by itself above. We can do this:

(register-handler
   :delete-item
   (trim-debug delete-handler))    ;; <== used like "trim-v"

(register-handler
   :delete-item
   trim-debug               ;;  3-arity allows middleware to be supplied 
   delete-handler)   

What does debug do? Well, it side effects and writes interesting stuff to the console. It is a wrapping which tells us what the ham has done.

trim-v was an adaptor. debug side-effects. Both are middleware. And they compose via comp.

Realise also that we believe in data > functions > macros so you can supply a vector of middleware to the 3-arity version:

(register-handler
   :delete-item
    [trim-v  (when ^boolean goog.DEBUG debug)]  ;; middleware supplied as data
   delete-handler-2) ;; <== handler here

register-handler will take the vector you supply, remove any nils (I'm looking at that when above) and comp the result for you. By dealing in vectors, we are dealing with data.

In fact, register-handler will flatten the middleware before it does a comp, so you could even supply a vector like this: [trim-v [debug another]] and it would be flattened and comped. This is only useful when you are incrementally building up your middleware as data in the first place.

5. Middleware Factories

Sometimes you need to parameterise the actions of a Middleware.

trim-v and debug are Middleware but so is this: (path [:some :where]). Oooohh look, parameters.

path is known as a Middleware Factory. A function which returns Middleware, once you give it some parameters. If you must know, it works a bit like update-in but that's not important right now. Just know it is factory function which produces middleware.

Use it like this:

(def middle-w   (comp (path [:some :path]) 
                      trim-v
                      debug))   ;; 3 step pipeline

6. Ordering

It turns out debug is order dependent wrt to trim-v:

(comp  debug trim-v)   <= not quite the same =>   (comp trim-v debug)

trim-v does the same job in either position, but debug logs either the full event [:delete-item 42] or the trimmed event [42] depending on whether it comes before or after trim-v when the pipeline is run.

(comp trim-v debug)      ;;  debug logs the trimmed event vector

The other way around:

(comp  debug trim-v)       ;; debug logs the original, full event vector

When we use comp with middleware it is the middleware on the left which is executed first when the pipeline is run.

Wait, what? Can that be right? Look at this:

((comp count str inc) 99)      ;; right-to-left:  first inc, then str, then count
;; => 3

So why am I telling you that the left-most middleware will run first? Why am I saying this:

(comp debug trim-v)    ;; debug runs first, then trim-v

Well, I'm saying that because I'm talking about the "running" of the pipeline, not the building of the pipeline.

Yes, when the pipeline is being built by comp, trim-v is applied first, and that means it will be the closest bread wrapping that hamy handler. And then debug will be the outside layer of bread again.

Which means... when later it comes time for us to eat this sandwich, which layer of bread does our teeth hit first? The outside most bread wrapping. The last wrapping which was applied. The one left most in the comp.

So at "pipeline use time" (sandwich eating time) it is the leftmost middleware that happens first, rather than "building pipeline time" (sandwich making time) when it is the rightmost middleware which is put closest to the ham.

Confused? Slightly hungry? Sure. Me too. Don't even try to work it out. Just remember it. The leftmost middleware happens first when an event is being processed.

That's about 90% of the battle.

Next, you can look into writing your own middleware.