-
-
Notifications
You must be signed in to change notification settings - Fork 715
Using Handler Middleware
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"?
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.
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:
- see how to use them, then
- see how to write them
This page deals with "using them", and there's a 2nd page on "writing them".
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:
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]])
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)
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.
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 flatten
ed and comp
ed. This is only useful when you are incrementally building up your middleware as data in the first place.
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
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.
Deprecated Tutorials:
Reagent: