Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.Sign up
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
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/compis 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"
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
- 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-dbmatches 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
- 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)
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
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
(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)
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
Realise also that we believe in
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.
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
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.
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
(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
 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
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
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.