-
-
Notifications
You must be signed in to change notification settings - Fork 715
Effectful Event Handlers
This page describes features in the upcoming v0.8.0 release of re-frame.
This tutorial shows you how to implement side-effecting event handlers in a pure way.
In the process, because of the explanations given, you'll also get insight
into how you might use an alternative to app-db
, like a DataScript database.
Not all event handlers are pure. The majority are, but there'll always be some handlers which need to side-effect.
First, the good.
Here we register a pure event handler:
(register-handler
:my-event
(fn [db [_ a]] ;; <--- lovely and pure
(assoc db :flag true)))
The event handler registered here is passed values, returns a value. No side-effects.
Now, to the more troublesome.
This handler is not pure:
(register-handler
:my-event
(fn [db [_ a]]
(dispatch [:do-something-else 3]) ;; oops, side-effect
(assoc db :flag true)))
It is passed values, returns a value. But... it
has a side-effect. That dispatch
queues up another event to be processed. It changes the world.
And, this is not pure either:
(register-handler
:my-event
(fn [db [_ a]]
(GET "http://json.my-endpoint.com/blah" ;; dirty great big side-effect
{:handler #(dispatch [:process-response %1])
:error-handler #(dispatch [:bad-response %1])})
(assoc db :flag true)))
Sure, this approach works. But that dirty great big side-effect doesn't come for free.
Both of those impure event handlers work. So what's the problem?
In theory, a re-frame application proceeds step by step, like a reduce:
at any one time, the value in app-db is the result of performing a reduce over the entire collection of events dispatched in the app up until that time. The combining function for this reduce is the set of registered event handlers.
Such a collection of events is replayable and that's a dream for debugging. But only when all the handlers are pure. Handlers with side-effects (like HTTP GETs) pollute the replay process, inserting extra events, etc, which ruins the process.
And it is hard to test these impure handlers. Did the handler GET the right URL?
Did it dispatch the right event? Now we have to start mocking things out. Everything
gets harder.
So, if you end up with impure handlers, you end up with a small collection of irritating paper cuts. It isn't a disaster, but we'd like better.
Side-effecting event handlers are inevitable. Event handlers implement the control logic in your app, and that means dealing with the mutative outside world of servers, databases, windows.location, cookies, etc.
There's just not a lot of getting around that.
So, end of story? No solution?
Well, it turns out there is a solution. There is a way to create pure event handlers which cause effects, without actually doing the side-effects.
Above, I claimed that this fn
event handler was pure:
(register-handler
:my-event
(fn [db _]
(assoc db :flag true)))
Takes a db
value, returns a db
value. No side-effects. Pure!
All true, but ... this purity is only possible because OTHER parts of re-frame are doing the necessary side-effecting.
Wait. What "necessary side-effecting"?
We'll, app-db
is a ratom (containing application state) and it has to be updated, right,
with the value returned by the handler. Here's the sequence carried out by
re-frame for each event which is handled:
- extract the value from
app-db
(a ratom) - call the event handler with this
db
value as the first argument - capture the amended return value, and
- use
reset!
to store the amended value back intoapp-db
.
So, you only get to live in your nicce pure world because re-frame is looking after the necessary
side-effect on app-db
.
The same pattern happens with Reagent/React too.
You write a nice pure component:
(defn say-hi
[name]
[:div "Hello " name])
and behind the scenes, React makes it happen, by mutating the DOM for you. It too is looking after the necessary side-effects.
Pause and look back at say-hi
. I'd like you to view it through the following prism: it is a pure
function which returns a description of the side-effects required. It says: add a div element
to the DOM.
Plus, notice that the description is declarative. We don't tell React how to do it.
Plus, notice that it is data. Hiccup is just vectors and maps.
This is a big, important concept. There's just no getting away from certain side-effects, right? But we can program via pure functions which describe side-effects, declaratively, in data and let the backing framework look after making them happen. Efficiently. Discreetly.
You are already using this technique. We now just need to use it to solve the side-effecting handler problem.
From here, two steps:
- Work out how event handlers can declaratively describe side-effects, in data.
- Work out how re-frame can do the "necessary side-effecting". Efficiently and discreetly.
So, how would it look if event handlers returned side-effects, declaratively, in data?
Here is an inpure handler:
(register-handler
:my-event
(fn [db [_ a]]
(dispatch [:do-something-else 3]) ;; Ekkk, side-effect
(assoc db :flag true)))
Here it is re-written so it is pure:
(register-handler
:my-event
(fn [db [_ a]]
{:db (assoc db :flag true) ;; side-effect we want on db
:dispatch [:do-something-else 3]})) ;; side-effect from dispatching
The handler is returning a data structure which describes two side-effects:
- update app-db (application state) with this new
db
value - dispatch an event
The impure handler caused a dispatch
side-effect, and the pure handler describes
a dispatch
side-effect.
The impure way:
(register-handler
:my-event
(fn [db [_ a]]
(GET "http://json.my-endpoint.com/blah" ;; dirty great big side-effect
{:handler #(dispatch [:process-response %1])
:error-handler #(dispatch [:bad-response %1])})
(assoc db :flag true)))
the pure way:
(register-handler
:my-event
(fn [db [_ a]]
{:html {:method :get
:url "http://json.my-endpoint.com/blah"
:on-success [:process-blah-response]
:on-fail [:failed-blah]}
:db (assoc db :flag true)}))
Again, the old way causes a side-effect (Booo!) and the new way describes, declaratively, in data, the side-effects required (Yaaa!).
All good progress so far, but time now to take the next step.
Above, I proposed this event handler:
(register-handler
:my-event
(fn [db [_ a]]
{:db (assoc db :flag true) ;; side-effect we want on db
:dispatch [:do-something-else 3]})) ;; side-effect from dispatching
I'd now like to change the name of the first handler argument from db
to world
,
and pass in this structure {:db db}
. Ie. the argument is now a map, and the
key :db
has the value in app-db
.
So, we rewrite our handler like this:
(register-handler
:my-event
(fn [world [_ a]] ;; world, not db as parameter
{:db (assoc-in world [:db :flag] true)
:dispatch [:do-something-else 3]}))
or maybe like this (shrug):
(register-handler
:my-event
(fn [world [_ a]] ;; world, not db as parameter
(-> world ;; world is {:db db}
(assoc-in [:db :flag] true)
(assoc-in [:db :dispatch] [:do-something-else 3]))))
View it like this, world
is the initial "state of the world". And the handler returns
the side-effects required to create the new world.
Just to state the obvious, thsis rewirte means that world
can now be more than db
. Anything can be supplied.
Perhaps there's no db
at all, and instead a DataScript database should be provided.
"world" is now whatever state our handlers need to be aware of. Most of the time, that's just what's in app-db, but "other" is possible.
But, to make any opf this happen, you must know how "world" is currently created ...
A new "world" is created and given to each event handler as its first argument.
"worlds" get created by an event handler's middleware stack (each event handler can have its own stack).
When you do this:
(register-handler
:my-event ;; no apparent middleware !!!!!
(fn [db _]
(assoc db :flag true)))
it doesn't appear as if any middleware is given.
But don't be fooled. re-frame.core/register-handler
exists for only one reason: to install the pure
middleware
on all your handlers in the right place within the stack. Proof
So why is pure
so important?
It is pure
which takes the value out of app-db
, makes it "the world" by providing
it to the event handler as the first argument and then, after the handler returns, it is again
pure
which uses swap!
to mutate app-db
.
So pure
is the bit of re-frame which does the "necessary side-effecting" we talked about above.
pure
is pretty central to everything, but re-frame's other standard middleware also
do side-effects so that your handler doesn't have to.
For example, undoable
, snapshots the value in app-db
. And after
is used in the todomvc
example to write data to LocalStore.
So, within re-frame, there is a strong tradition of handling side-effects in middleware.
So, how we going here?
Earlier, I presented a two step plan:
- Work out how event handlers can declaratively describe side-effects, in data.
- Work out how re-frame can action these side-effecting descriptions, efficiently and discreetly
We have a solution for point 1.
And, regarding point 2, we know that middleware is the answer, somehow. We need middleware designed to action side-effects.
remember that each event handler has its own middleware stack. Genuinely pure handlers can continue on as is. No change. No change to their middleware stack. But side-effecting handler can be given this new middleware.
Below I provide a specific solution but, remember, you can write you own middleware.
XXX this can all be madeto happen without any change to re-frame.
We have the solution is going to come via middleware. We have choices like:
- Come up with a complete alternative to
pure
which understands We need a more capablepure
. Let's explore this now.
XXX Register Effect Handlers
We're not going to change pure
. We're going to keep it exactly where it is ... after all, most handlers are pure.
But, when we are writing a side-effecting handler, we're going to add another middleware to the stack
for that particular handler. We're going to call this new middleware fx
(oh yeah, geddit, effects).
The job of fx
is two fold:
- It will change the world from
db
to{:db db}
. This is what the rest of the stack will see, including the handler being registered. - After the handler has run, it will interpret and action the side-effects requested in the data structure returned.
Here is an initial sketch of fx
in code:
(defn fx
[handler]
(fn fx-handler
[db event-vec]
(let [world {:db db} ;; world was `db`, make it `{:db db}`
result (handler world event-vec)] ;; call the event handler with world
; HERE IS WHERE WE ACTION THE SIDE-EFFECTS IN result
(:db result))))
If you don't understand any of this middleware stuff, don't worry too much.
Just know that we now have to solve a problem. It is down to this fx
wrapper to action the side-effects provided by the handler.
The event handler might return something like:
{:db X
:http Y
:dispatch Z}
The :db
part is easy. pure
handles that if we return it (which we do, see {:db result}
). But
what about :http
abd :dispatch
. And what if fx
got something unknown like :abc
?
How?
So this is the interesting bit.
There's two options:
- make
fx
more capable and have it know how to do the side-effects (via a registration scheme described below) - keep
fx
simple and do all side-effect via middleware.
Whatever solution we create has to be both extensible and swapable.
Here's some candidate side-effects:
- dispatch
- flush-dom (force dom writes)
- undo
- localstore writes
- database queries (rethinkdb) ?
- http get or post
- cookies
But this list is open ended. Your app may need others.
So, the mechanism fx
uses for knowing about, and handing, side-effects
will have to be extensible. Plugable.
When fx
finds that the returned map has an :http
key, it needs to
look up the side-effect Handler which can cause that to happen.
So there will be a way for apps to register side-effect Handlers.
Register a function to handle the side-effect :http
. Ie. if the event handler
returns a map containing an :http
key, then this function is registered to
too process that side-effect (using the information within that sub tree).
(register-effect-handler
:http
(fn [http] ;; given the value of the `:http` key
(GET (:url http)
{:handler #(dispatch (conj (:on-success http) %1))
:error-handler #(dispatch (conj (:on-fail http) %1]))})))
There might be standard effect handlers provided. But writeyour own.
Sometimes you don't want the side-effects to happen. Sometimes, if you are testing or tracking down a bug, by replaying events, you don't want the database mutation side-effects to actually run.
So you need a way to switch side-effect instructions into noops. And then back again into being performed.
XXX how would you do multiple GET ? Give a vector I guess
So we're going to need a new bit of middleware, called fx
.
We're going to need a new
Deprecated Tutorials:
Reagent: