Skip to content

Effectful Event Handlers

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

This document is a work in progress.

This document describes features in the upcoming v0.8.0 release of re-frame.

This page will show you how to implement side-effecting event handlers.

You'll also get insight into how you might use alternatives to app-db, like DataScript.

The Great Unwashed

Not all event handlers are pure. Most are, but there'll always be some that need to side-effect.

Impure handlers create problems when it comes to testing, event replay-ablity, etc. So, we want to avoid them.

Species Of Handler

First, the good - we register a pure event handler:

(register-handler
   :my-event
   (fn [db [_ a]]    ;; <--- lovely and pure
       (assoc db :flag true)))

This event handler is passed values, returns values. No side effects. Lovely.

Now the bad.

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 side effects. That dispatch queues up another event to be processed. It changes the world.

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

This will work. But again, there are side effects.

Why Is This bad, Again?

It is bad in theory, and irritating in practice.

In theory, a re-frame application proceeds like this:

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 event handlers.

But this assumes pure handlers. If the handlers have side effect (like HTTP GETs), then you can't just replay a recorded collection of events and end up in a certain place. The neatness of the model goes out the window.

This theoretical problem, tends to ooze through in other practical ways. For example, it is hard to test side effecting handlers. Did the handler HTTP Get to the right URL?
Did it dispatch the right event? Awkward.

The whole thing is like an irritating pebble in your shoe. It isn't a disaster, but it is untidy. We want better.

Why Aren't They All Pure?

Certain handlers simply can't do their job unless they can cause a side effect. There's 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 handlers which cause effects, without actually doing the side effects.

Pure, Really?

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 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 get to write a pure function because re-frame looks after the necessary side effect on app-db.

Et tu, React?

This 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.

Pattern Details

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. It returns vectors and maps.

This is a big 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.

Back To Event Handlers

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 is the pure way:

(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 with this new db value
  2. dispatch an event

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

For the moment, we just trust that the framework/context can perform the described side effects. More on that soon.

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 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 give it this structure {:db db}. Ie. the argument is now a map, and the key :db has the value in app-db.

Now 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.

The Input World

Realise that world doesn't have to contain db. Anything can be supplied. Perhaps there's no db at all, and instead a DataScript database should be provided.

But, to make that happen, you must know how "world" is currently created ...

New World

"worlds" get created by your middleware 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. 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.

Meet fx

So, here's the plan. We're going to keep pure exactly where it is ... after all most handlers are indeed 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:

  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 action the side effects requested by the event handler.

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 handler returns 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 we got something unknown like :abc?

How?

Actioning Side Effects

There's two options I can see:

  • make fx more capable and have it know how to do the side effects (via a registration scheme described below)
  • do all side effect via middleware. Keep fx simple.

But how to write this effects middleware? How can effects turn descriptions of side effects into real side effects?

The solution has to be 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 not definitive. Your app may need others. So, the mechanism effects' uses for knowing about and handing side effects will have to be extensible. Plugable.

When effects finds that the returned map has an :http key, it need 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.

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 you need a way to switch side effect instructions into noops. And then back again into being performed.

Ordering Of Effects?

XXX undo should come before reset db XXX how would you do multiple GET ? Give a vector I guess

Summary:

So we're going to need a new bit of middleware, called effects.

We're going to need a new