re-frame/docs/Interceptors.md

400 lines
15 KiB
Markdown
Raw Normal View History

## re-frame Interceptors
2016-08-13 16:34:25 +10:00
This tutorial explains re-frame Interceptors. By the end, you'll much
better understand the mechanics of event handling.
As you read this, refer back to the 3rd panel of the
[Infographic](EventHandlingInfographic.md).
2016-12-20 23:02:13 +11:00
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- ## Table Of Contents -->
<!-- - [Why Interceptors?](#why-interceptors) -->
<!-- - [What Do Interceptors Do?](#what-do-interceptors-do) -->
<!-- - [Wait, I know That Pattern!](#wait-i-know-that-pattern) -->
<!-- - [What's In The Pipeline?](#whats-in-the-pipeline) -->
<!-- - [Show Me](#show-me) -->
<!-- - [Handlers Are Interceptors Too](#handlers-are-interceptors-too) -->
<!-- - [Executing A Chain](#executing-a-chain) -->
<!-- - [The Links Of The Chain](#the-links-of-the-chain) -->
<!-- - [What Is Context?](#what-is-context) -->
<!-- - [Self Modifying](#self-modifying) -->
<!-- - [Credit](#credit) -->
<!-- - [Write An Interceptor](#write-an-interceptor) -->
<!-- - [Wrapping Handlers](#wrapping-handlers) -->
<!-- - [Summary](#summary) -->
<!-- - [Appendix](#appendix) -->
<!-- - [The Built-in Interceptors](#the-built-in-interceptors) -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2016-08-13 16:34:25 +10:00
2016-12-20 23:02:13 +11:00
## Why Interceptors?
2016-08-11 14:33:28 +10:00
2016-08-11 16:09:43 +10:00
Two reasons.
__First__, we want simple event handlers.
2016-08-11 14:33:28 +10:00
Interceptors can look after "cross-cutting" concerns like undo, tracing and validation.
They help us to factor out commonality, hide complexity and introduce further steps into the "Derived Data,
Flowing" story promoted by re-frame.
2016-08-11 14:33:28 +10:00
So, you'll want to use Interceptors because they solve problems, and help you to write nice code.
2016-08-11 14:33:28 +10:00
__Second__, under the covers, Interceptors provide the mechanism by which
2016-08-19 14:47:37 -04:00
event handlers are executed (when you `dispatch`). They are a central concept.
2016-08-11 14:33:28 +10:00
2016-12-20 23:02:13 +11:00
## What Do Interceptors Do?
2016-08-11 14:33:28 +10:00
They wrap.
2016-08-11 14:33:28 +10:00
Specifically, they wrap event handlers.
2016-08-11 14:33:28 +10:00
2016-08-13 16:34:25 +10:00
Imagine your event handler is like a piece of ham. An interceptor would be
2016-08-19 14:47:37 -04:00
like bread on either side of your ham, which makes a sandwich.
2016-08-11 14:33:28 +10:00
And two Interceptors, in a chain, would be 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.
2016-08-11 14:33:28 +10:00
Interceptors wrap on both sides of a handler, layer after layer.
2016-12-20 23:02:13 +11:00
## Wait, I know That Pattern!
2016-08-11 14:33:28 +10:00
2016-08-19 14:47:37 -04:00
Interceptors implement middleware. But differently.
2016-08-11 14:33:28 +10:00
Traditional middleware - often seen in web servers - creates a data
processing pipeline via the nested composition of higher order functions.
The result is a "stack" of functions. Data flows through this pipeline,
2016-08-11 14:33:28 +10:00
first forwards from one end to the other, and then backwards.
Interceptors achieve the same outcome by assembling functions, as data,
2016-08-11 14:33:28 +10:00
in a collection (a chain, rather than a stack). Data can then be iteratively
pipelined, first forwards through the functions in the chain,
and then backwards along the same chain.
2016-08-11 14:33:28 +10:00
Because the interceptor pipeline is composed via data, rather than
higher order functions, it is a more flexible arrangement.
2016-08-11 14:33:28 +10:00
2016-12-20 23:02:13 +11:00
## What's In The Pipeline?
2016-08-11 14:33:28 +10:00
Data. It flows through the pipeline being progressively transformed.
2016-08-11 14:33:28 +10:00
Fine. But what data?
With a web server, the middleware "stack" progressively
transforms a `request` in one direction, and, then in the backwards
sweep, it progressively produces a `response`.
2016-08-11 14:33:28 +10:00
In re-frame, the forwards sweep progressively creates the `coeffects`
(inputs to the event handler), while the backwards sweep processes the `effects`
(outputs from the event handler).
2016-08-11 14:33:28 +10:00
I'll pause while you read that sentence again. That's the key
concept, right there.
2016-08-11 14:33:28 +10:00
2016-12-20 23:02:13 +11:00
## Show Me
2016-08-11 14:33:28 +10:00
At the time when you register an event handler, you can provide a chain of interceptors too.
2016-08-11 14:33:28 +10:00
Using a 3-arity registration function:
```clj
(reg-event-db
2016-08-11 14:33:28 +10:00
:some-id
[in1 in2] ;; <--- a chain of 2 interceptors
2016-08-11 16:09:43 +10:00
(fn [db v] ;; <-- the handler here, as before
2016-08-11 14:33:28 +10:00
....)))
```
> Each Event Handler can have its own tailored interceptor chain, provided at registration-time.
2016-08-11 14:33:28 +10:00
2016-12-20 23:02:13 +11:00
## Handlers Are Interceptors Too
2016-08-11 14:33:28 +10:00
You might see that registration above as associating `:some-id` with
2016-12-20 23:02:13 +11:00
two things: (1) a chain of 2 interceptors `[in1 in2]`
and (2) a handler.
2016-09-07 15:16:57 +02:00
Except, the handler is turned into an interceptor too (we'll see how shortly).
So `:some-id` is only associated with one thing: a 3-chain of interceptors,
with the handler wrapped in an interceptor, called say `h`, and put on the
end of the other two: `[in1 in2 h]`.
Except, the registration function itself, `reg-event-db`, actually takes this 3-chain
2016-09-07 15:40:52 +10:00
and inserts its own standard interceptors, called say `std1` and `std2`
(which do useful things, more soon) at the front,
2016-12-20 23:02:13 +11:00
so **ACTUALLY**, there's about 5 interceptors in the chain: `[std1 std2 in1 in2 h]`
2016-08-11 14:33:28 +10:00
So, ultimately, that event registration associates the event id `:some-id`
2016-08-25 18:40:07 +10:00
with __just__ a chain of interceptors. Nothing more.
Later, when a `(dispatch [:some-id ...])` happens, that 5-chain of
interceptors will be "executed". And that's how an event gets handled.
2016-08-13 16:34:25 +10:00
2016-08-11 14:33:28 +10:00
2016-08-13 16:34:25 +10:00
## Executing A Chain
2016-08-17 00:08:30 +10:00
### The Links Of The Chain
2016-08-11 14:33:28 +10:00
Each interceptor has this form:
```clj
2016-08-11 16:09:43 +10:00
{:id :something ;; decorative only
2016-08-11 14:33:28 +10:00
:before (fn [context] ...) ;; returns possibly modified context
:after (fn [context] ...)} ;; `identity` would be a noop
```
That's essentially a map of two functions. Now imagine a vector of these maps - that's an interceptor chain.
2016-09-07 15:40:52 +10:00
Above we imagined an interceptor chain of `[std1 std2 in1 in2 h]`. Now we know that this is really
a vector of 5 maps: `[{...} {...} {...} {...} {...}]` where each of the 5 maps have
2016-09-07 15:40:52 +10:00
a `:before` and `:after` fn.
Sometimes, the `:before` and `:after` fns are noops (think `identity`).
2016-08-11 14:33:28 +10:00
2016-08-11 16:09:43 +10:00
To "execute" an interceptor chain:
2016-08-11 14:33:28 +10:00
1. create a `context` (a map, described below)
2016-08-11 16:09:43 +10:00
2. iterate forwards over the chain, calling the `:before` function on each interceptor
3. iterate over the chain in the opposite direction calling the `:after` function on each interceptor
2016-08-11 14:33:28 +10:00
Remember that the last interceptor in the chain is the handler itself (wrapped up to be the `:before`).
2016-08-11 16:09:43 +10:00
That's it. That's how an event gets handled.
### What Is Context?
2016-08-11 14:33:28 +10:00
Some data called a `context` is threaded through all the calls.
2016-08-11 16:09:43 +10:00
This value is passed as the argument to every `:before` and `:after`
function and it is returned by each function, possibly modified.
2016-08-11 14:33:28 +10:00
A `context` is a map with this structure:
2016-08-11 14:33:28 +10:00
```clj
2016-08-11 16:09:43 +10:00
{:coeffects {:event [:some-id :some-param]
2016-08-11 14:33:28 +10:00
:db <original contents of app-db>}
2016-08-11 14:33:28 +10:00
:effects {:db <new value for app-db>
:dispatch [:an-event-id :param1]}
2016-08-11 14:33:28 +10:00
:queue <a collection of further interceptors>
:stack <a collection of interceptors already walked>}
```
`context` has `:coeffects` and `:effects` which, if this was a web
2016-08-11 16:09:43 +10:00
server, would be somewhat analogous to `request` and `response`
2016-08-11 14:33:28 +10:00
respectively.
2016-08-11 23:24:39 +10:00
`:coeffects` will contain the inputs required by the event handler
(sitting presumably on the end of the chain). So that's
2016-08-17 00:08:30 +10:00
data like the `:event` being processed, and the initial state of `db`.
2016-08-11 14:33:28 +10:00
The handler-returned side effects are put into `:effects` including,
2016-08-11 14:33:28 +10:00
but not limited to, new values for `db`.
The first few interceptors in a chain (inserted by `reg-event-db`)
2016-08-11 14:33:28 +10:00
have `:before` functions which __prime__ the `:coeffects`
2016-08-11 16:09:43 +10:00
by adding in `:event`, and `:db`. Of course, other interceptors can
2016-08-17 00:08:30 +10:00
add further to `:coeffects`. Perhaps the event handler needs
data from localstore, or a random number, or a
DataScript connection. Interceptors can build up `:coeffects`, via their
`:before`.
2016-08-11 14:33:28 +10:00
Equally, some interceptors in the chain will have `:after` functions
2016-08-11 16:09:43 +10:00
which process the side effects accumulated into `:effects`
2016-08-19 14:47:37 -04:00
including, but not limited to, updates to app-db.
2016-08-11 14:33:28 +10:00
2016-08-17 00:08:30 +10:00
### Self Modifying
2016-08-11 14:33:28 +10:00
Through both stages (before and after), `context` contains a `:queue`
of interceptors yet to be processed, and a `:stack` of interceptors
2016-08-12 23:49:15 +10:00
already done.
2016-08-11 14:33:28 +10:00
2016-08-13 16:34:25 +10:00
In advanced cases, these values can be modified by the Interceptor
functions through which the `context` is threaded.
2016-08-11 14:33:28 +10:00
What I'm saying is that interceptors can be dynamically added
and removed from the `:queue` by existing Interceptors.
2016-08-11 14:33:28 +10:00
### Credit
2016-08-12 23:49:15 +10:00
> All truths are easy to understand once they are discovered <br>
> -- Galileo Galilei
2017-03-29 14:50:47 +02:00
> Things always become obvious after the fact <br>
> -- Nassim Nicholas Taleb
2016-08-11 14:33:28 +10:00
This elegant and flexible arrangement was originally
designed by the talented
[Pedestal Team](https://github.com/pedestal/pedestal/blob/master/guides/documentation/service-interceptors.md). Thanks!
2016-08-11 14:33:28 +10:00
### Write An Interceptor
Dunno about you, but I'm easily offended by underscores.
If we had a view which did this:
2016-08-11 14:33:28 +10:00
```clj
(dispatch [:delete-item 42])
```
We'd have to write this handler:
2016-08-11 14:33:28 +10:00
```clj
(reg-event-db
2016-08-11 14:33:28 +10:00
:delete-item
(fn
2016-09-07 15:16:57 +02:00
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
2016-08-11 14:33:28 +10:00
(dissoc db key-to-delete)))
```
Do you see it there? In the event destructuring!!! Almost mocking us with that
2016-08-11 16:09:43 +10:00
passive aggressive, understated thing it has going on!! Co-workers
have said I'm "being overly sensitive", perhaps even pixel-ist, but
2016-09-07 15:16:57 +02:00
you can see it, right? Of course you can.
2016-08-11 14:33:28 +10:00
2016-09-07 15:40:52 +10:00
What a relief it would be to not have it there, but how? We'll write an interceptor: `trim-event`
2016-08-11 14:33:28 +10:00
Once we have written `trim-event`, our registration will change to look like this:
```clj
(reg-event-db
2016-08-11 14:33:28 +10:00
:delete-item
[trim-event] ;; <--- interceptor added
(fn
[db [key-to-delete]] ;; <---yaaah! no leading underscore
2016-08-11 14:33:28 +10:00
(dissoc db key-to-delete)))
```
`trim-event` will need to change the `:coeffects` map (within `context`). Specifically, it will be
changing the `:event` value within the `:coeffects`.
2016-08-11 14:33:28 +10:00
`:event` will start off as `[:delete-item 42]`, but will end up `[42]`. `trim-event` will remove that
leading `:delete-item` because, by the time the event is
2016-08-23 09:12:35 +02:00
being processed, we already know what id it has.
2016-08-11 14:33:28 +10:00
And, here it is:
2016-08-11 14:33:28 +10:00
```clj
(def trim-event
(re-frame.core/->interceptor
2016-08-11 23:24:39 +10:00
:id :trim-event
2016-08-11 14:33:28 +10:00
:before (fn [context]
(let [trim-fn (fn [event] (-> event rest vec))]
(update-in context [:coeffects :event] trim-fn)))))
2016-08-11 14:33:28 +10:00
```
As you read this, look back to what a `context` looks like.
2016-08-11 14:33:28 +10:00
Notes:
2016-08-17 00:08:30 +10:00
1. We use `->interceptor` to create an interceptor (which is just a map)
2. Our interceptor only has a `:before` function
2016-08-11 14:33:28 +10:00
3. Our `:before` is given `context`. It modifies it and returns it.
4. There is no `:after` for this Interceptor. It has nothing to do
with the backwards processing flow of `:effects`. It is concerned only
2016-08-17 00:08:30 +10:00
with `:coeffects` in the forward flow.
### Wrapping Handlers
2016-08-11 14:33:28 +10:00
We're going well. Let's do an advanced wrapping.
2016-08-11 14:33:28 +10:00
Earlier, in the "Handlers Are Interceptors Too" section, I explained that `event handlers`
are wrapped in an Interceptor and placed on the end of an Interceptor chain. Remember the
whole `[std1 std2 in1 in2 h]` thing?
2016-09-07 15:40:52 +10:00
2016-12-16 23:25:46 -08:00
We'll now look at the `h` bit. How does an event handler get wrapped to be an Interceptor?
2016-08-11 16:09:43 +10:00
Reminder - there's two kinds of handler:
2016-08-11 14:33:28 +10:00
- the `-db` variety registered by `reg-event-db`
- the `-fx` variety registered by `reg-event-fx`
I'll now show how to wrap the `-db` variety.
2016-09-07 15:40:52 +10:00
Reminder: here's what a `-db` handler looks like:
2016-08-11 16:09:43 +10:00
```clj
2016-09-07 15:16:57 +02:00
(fn [db event] ;; takes two params
2016-08-11 16:09:43 +10:00
(assoc db :flag true)) ;; returns a new db
```
So, we'll be writing a function which takes a `-db` handler and returns an
Interceptor which wraps that handler:
2016-08-11 14:33:28 +10:00
```clj
(defn db-handler->interceptor
[db-handler-fn]
(re-frame.core/->interceptor ;; a utility function supplied by re-frame
:id :db-handler ;; ids are decorative only
2016-08-11 14:33:28 +10:00
:before (fn [context]
(let [{:keys [db event]} (:coeffects context) ;; extract db and event from coeffects
new-db (db-handler-fn db event)] ;; call the handler
2016-08-11 14:33:28 +10:00
(assoc-in context [:effects :db] new-db)))))) ;; put db back into :effects
```
Notes:
1. Notice how this wrapper extracts data from the `context's` `:coeffects`
and then calls the handler with that data (a handler must be called with `db` and `event`)
2. Equally notice how this wrapping takes the return value from the `-db`
2016-10-13 17:44:29 +13:00
handler and puts it into `context's` `:effects`
2016-10-13 00:35:37 -04:00
3. The modified `context` (it has a new `:effects`) is returned
3. This is all done in `:before`. There is no `:after` (it is a noop). But this
could have been reversed with the work happening in `:after` and `:before` a noop. Shrug.
Remember that this Interceptor will be on the end of a chain.
Feeling confident? Try writing the wrapper for `-fx` handlers - it is just a small variation.
2016-08-11 14:33:28 +10:00
2016-08-13 16:34:25 +10:00
## Summary
2016-08-11 14:33:28 +10:00
In this tutorial, we've learned:
__1.__ When you register an event handler, you can supply a collection of interceptors:
2016-08-17 00:08:30 +10:00
```clj
(reg-event-db
2016-08-11 16:09:43 +10:00
:some-id
[in1 in2] ;; <--- a chain of 2 interceptors
2016-08-11 16:09:43 +10:00
(fn [db v] ;; <-- real handler here
....)))
```
2016-08-17 00:08:30 +10:00
__2.__ When you are registering an event handler, you are associating an event id with a chain of interceptors including:
2016-09-07 15:16:57 +02:00
- the ones you supply (optional)
- an extra one on the end, which wraps the handler itself
- a couple at the beginning of the chain, put there by the `reg-event-db` or `reg-event-fx`.
__3.__ An Interceptor Chain is executed in two stages. First a forwards sweep in which
all `:before` functions are called, and then second, a backwards sweep in which the
2016-08-17 00:08:30 +10:00
`:after` functions are called. A `context` will be threaded through all these calls.
__4.__ Interceptors do interesting things:
- add to coeffects (data inputs to the handler)
- process side effects (returned by a handler)
- produce logs
- further process
In the next Tutorial, we'll look at (side) Effects in more depth. Later again, we'll look at Coeffects.
2016-08-11 14:33:28 +10:00
2016-08-13 16:34:25 +10:00
## Appendix
2016-08-11 14:33:28 +10:00
2016-12-16 23:25:46 -08:00
### The Built-in Interceptors
2016-08-11 14:33:28 +10:00
2016-12-16 23:25:46 -08:00
re-frame comes with some built-in Interceptors:
2016-08-11 14:33:28 +10:00
- __debug__: log each event as it is processed. Shows incremental [`clojure.data/diff`](https://clojuredocs.org/clojure.data/diff) reports.
- __trim-v__: a convenience. More readable handlers.
2016-08-11 14:33:28 +10:00
And some Interceptor factories (functions that return Interceptors):
- __enrich__: perform additional computations (validations?), after the handler has run. More derived data flowing.
- __after__: perform side effects, after a handler has run. Eg: use it to report if the data in `app-db` matches a schema.
2016-08-11 14:33:28 +10:00
- __path__: a convenience. Simplifies our handlers. Acts almost like `update-in`.
In addition, [a Library like re-frame-undo](https://github.com/Day8/re-frame-undo) provides an Interceptor
2016-08-11 14:33:28 +10:00
factory called `undoable` which checkpoints app state.
2016-08-11 14:33:28 +10:00
To use them, first require them:
```Clojure
(ns my.core
(:require
[re-frame.core :refer [debug path]])
```
***
Previous: [Effectful Handlers](EffectfulHandlers.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effects](Effects.md)