Skip to content

Effectful Event Handlers

Mike Thompson edited this page Jun 19, 2016 · 25 revisions

This page describes features which may appear in the upcoming v0.8.0 release of re-frame. Comment and discussion on this feature is happening here. Please feel free to correct any typos and mistakes. Thanks.

This tutorial shows you how to implement pure event handlers that side-effect. And, yes, I'm serious.

In the process, given then explanations provided, you'll also get insight into how you might use an alternative to app-db, like perhaps a DataScript database.

Event Handlers

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.

Why Is This bad, Again?

Both of those impure event handlers will 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 replay-able and that's a dream for debugging. But only when all the handlers are pure. Handlers with side-effects (like that HTTP GET, or the dispatch) 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 to do better.

Why The Side Effects?

Side-effecting event handlers are inevitable. Event handlers implement the control logic in your re-frame app, and that means dealing with the mutative world of servers, databases, windows.location, cookies, etc.

There's just not a lot of getting around that.

Sounds a dire diagnosis? 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.

Doing vs Causing

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:

  1. extract the value (a map) from app-db (a ratom)
  2. call the event handler with this db value as the first argument
  3. capture the amended return value, and
  4. use reset! to store the amended value back into app-db.

So, you only get to live in your nice pure world because re-frame is looking after the necessary side-effect on app-db.

Et tu, React?

It's the same pattern with Reagent/React.

You write a nice pure component:

(defn say-hi
  [name]
  [:div "Hello " name])

and React mutates the DOM for you. It too is looking after the necessary side-effects.

Pattern Structure

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. While we can't get away from certain side-effects, we can program using pure functions which describe side-effects, declaratively, in data and let the backing framework look after the "doing" of them. Efficiently. Discreetly.

You are already using this technique. We now just need to use it to solve the side-effecting handler problem.

The Two Part Plan

From here, two steps:

  1. Work out how event handlers can declaratively describe side-effects, in data.
  2. Work out how re-frame can do the "necessary side-effecting". Efficiently and discreetly.

Part 1 Of Plan

So, how would it look if event handlers returned side-effects, declaratively, in data?

Here is an impure 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:

  1. update app-db (application state) with this new db value
  2. dispatch an event

Above, the impure handler caused a dispatch side-effect, and the pure handler describes a dispatch side-effect.

Another Example

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, descriptive 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!).

Re-imagining db

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 [: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. Kinda.

Alternative Worlds

This rewrite 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 worlds" are possible.

But, to make any of this happen, you must know how "world" is currently created ...

New Worlds

Currently, a new "world" is created each time an event handler is run, and it is supplied as the first argument, often called db.

"worlds" get created by an event handler's middleware stack (each event handler can have its own stack).

That's probably surprising to you. When you do this:

(register-handler
   :my-event     ;; no apparent middleware !!!!!
   (fn [db _]
       (assoc db :flag true)))

it certainly doesn't appear as if there's any middleware on that handler.

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 middleware 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 previously. That's critical.

Aside

pure is pretty central to everything, but re-frame's other standard middleware often 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 out to LocalStore.

So, within re-frame, there is already a strong tradition of handling side-effects in middleware. We're just going to up the ante a bit.

Plan Review

So, how we going here?

Earlier, I presented a two step plan:

  1. Work out how event handlers can declaratively describe side-effects, in data.
  2. 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 now know that middleware is the answer, somehow.

Let's give this new middleware a name: io.

Below I provide a specific solution for io but before we get there:

  1. remember, you can write your own middleware. It isn't hard.
  2. io is definitely not the only way to do this.

Meet io

We're not going to change pure. It is still going to sit there in all middleware stacks, and it is still going to be "installed" by re-frame.core/register-handler. After all, most handlers are pure.

Aside: remember that each event handler has its own middleware stack.

But we are going to install a new piece of middleware, called io, when the event handler needs to cause side-effects.

So, registration might look like this:

(register-handler
   :my-event
   io                  ;; <---- new middleware
   (fn [world [_ a]]     
     {:db       (assoc-in world [:db :flag]  true)         
      :dispatch [:do-something-else 3]}))

or maybe we'll create a new registration function which adds it automatically

(register-handler-io      ;;  <--- use a different registration fn
   :my-event              ;; no explicit io middleware
   (fn [world [_ a]]     
     {:db       (assoc-in world [:db :flag]  true)         
      :dispatch [:do-something-else 3]}))

Aside: The name io is clearly a nod towards Haskell. The language. Not James Haskell, the current English openside breakaway in Rugby, who I often claim is functional but lazy. My Rugby friends have no idea what I'm talking about - "Mike, you're being too harsh. He's in career best form". Pearls before swine. Anyway, I almost called it fx. Your preference?

Implementing io

The job of io is two fold:

  1. 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.
  2. 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 io in code:

(defn io
  [handler]
  (fn io-handler
    [db event-vec]                    ;; given a world of `db`     
    (let [world   {:db db}            ;; makes world  `{:db db}` 
          result (handler world event-vec)] ;; call the event handler with world
       ; HERE IS WHERE WE ACTION THE SIDE-EFFECTS returned in results
      (:db result))))

If you don't understand any of this middleware stuff, it doesn't matter too much.

Just know that we now have to solve a problem. It is down to io to action the side-effects returned by the handler.

io may get a return value 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 and :dispatch. And what if io got something like :abc?

How?

Solutions

Whatever solution we create has to be both extensible and swapable.

Extensible

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 io uses for knowing about, and handing, side-effects will have to be extensible. Plugable.

When io finds that the returned map has an :http key, it needs to look up the Side-Effect Handler which can make it happen.

So there will be a way for apps to register Side-Effect Handler.

There might be standard effect handlers provided. But you can write your own.

Swapable

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 we 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

Side Effect Handlers

They will be registered like this:

(register-effects-handler
   :http         ;; the key in the returned value
   (fn [http]
       ....))

The http argument will be the value for that part of the return.

Like this:

{:method :get
 :url    "http://json.my-endpoint.com/blah"
 :on-success  [:process-blah-response]
 :on-fail     [:failed-blah]}

XXX should they be given the entire returned value, or just their part of the try?
XXX the process of turning them into Noops still has me puzzled. How to do that nicely.